90% 的人看完命令模式的代码会说:「这不就是策略模式吗?」长一样是真的,但它们要解决的问题完全相反:一个封装「动作」,一个封装「算法」。RocketMQ 的消息、MySQL 的 redo log、JDK 的 Runnable,全是命令模式,但从来没人把它们叫成策略模式。
我画一个类图给你看:
java
// 命令模式
public interface Command {
void execute();
}
public class SaveCommand implements Command {
public void execute() {
// 保存文件
}
}
// 策略模式
public interface Strategy {
void execute();
}
public class QuickSortStrategy implements Strategy {
public void execute() {
// 快速排序
}
}
一模一样对不对?两个接口都叫 execute,实现类都有 execute 方法。
面试的时候我问:「命令模式和策略模式有什么区别?」
候选人就开始支支吾吾:「命令模式有个......Receiver?策略模式没有?好像是?」
实际上它们的区别不在代码结构上,在意图上。
一句话区分:算法 vs 动作
- 策略模式 :封装算法。同一件事有不同实现,挑一个用。
- 命令模式 :封装动作。一件事做完后,可能要撤销、记录、排队、远程发送。
策略模式在意「怎么算 」,命令模式在意「做了什么、谁做的、能不能回退」。
java
// 策略模式
sortStrategy.sort(list); // 怎么排?快排、归并、堆排,随你挑
// 排序完成就完了,调用方不关心过程
// 命令模式
saveCommand.execute(); // 做了什么?保存文件
// 调用方需要:能不能撤销?谁执行的?什么时候执行的?
// 能不能把 saveCommand 序列化后发到另一台机器?
代码长一样,意图天差地别。
命令模式最值钱的特性:可撤销
命令模式之所以需要单独存在,是因为它能解决策略模式解决不了的事------撤销。
java
public interface Command {
void execute();
void undo(); // 撤销命令
}
public class AddTextCommand implements Command {
private final TextEditor editor;
private final String text;
private int cursorPosition;
public void execute() {
cursorPosition = editor.getCursorPosition();
editor.insert(text);
}
public void undo() {
editor.delete(text, cursorPosition); // 撤销:删掉插入的文字
}
}
public class TextEditor {
private final Deque<Command> history = new ArrayDeque<>();
public void executeCommand(Command cmd) {
cmd.execute();
history.push(cmd); // 压栈
}
public void undo() {
if (!history.isEmpty()) {
history.pop().undo(); // 弹出并撤销
}
}
}
// 使用
editor.executeCommand(new AddTextCommand(editor, "Hello"));
editor.executeCommand(new AddTextCommand(editor, " World"));
editor.undo(); // 撤销 " World"
editor.undo(); // 撤销 "Hello"
策略模式能做到吗?策略模式里,sort(list) 排完就完了,你不能让排序「撤销」。因为排序的结果不是一个可以反向的操作。
命令模式的核心是对动作的完整建模:执行前什么状态、执行后什么状态、怎么回退到执行前。
这个能力,Underscore / Redux / 任何状态管理库都在用 。你按 Ctrl+Z 撤销编辑,本质就是命令模式的 undo()。
真实生产中的命令模式:分布式任务队列
我做过一个订单系统,每来一个订单就要发短信、发邮件、扣库存、减积分。最初是同步调用:
java
public void createOrder(Order order) {
orderDao.save(order);
smsService.send(order.getUserId(), "订单创建成功");
emailService.send(order.getUserId(), "...");
inventoryService.deduct(order);
pointService.add(order.getUserId(), 10);
}
四个调用任何一个挂掉,整个订单流程就挂。短信接口超时 30 秒,用户要等 30 秒才能看到下单结果。
改成命令模式 + MQ:
java
public interface OrderCommand extends Runnable {
String getCommandId(); // 用于幂等
void execute(); // 不再是 run
}
public class SendSmsCommand implements OrderCommand {
private final Long userId;
private final String content;
public void execute() {
smsService.send(userId, content);
}
public String getCommandId() {
return "sms_" + userId + "_" + System.currentTimeMillis();
}
}
// 生产端
public void createOrder(Order order) {
orderDao.save(order);
// 每个动作变成一个命令,丢进 MQ
mqProducer.send(new SendSmsCommand(userId, "订单创建成功"));
mqProducer.send(new SendEmailCommand(userId, "..."));
mqProducer.send(new DeductInventoryCommand(order));
mqProducer.send(new AddPointCommand(userId, 10));
}
// 消费端:消息传来一个 OrderCommand,反射执行
public class OrderCommandConsumer {
@RabbitListener(queues = "order_command")
public void onMessage(OrderCommand cmd) {
// 幂等:同一个 commandId 不重复执行
if (dedupService.isProcessed(cmd.getCommandId())) return;
cmd.execute();
dedupService.markProcessed(cmd.getCommandId());
}
}
这套设计里,每个 SendSmsCommand 都是一个独立对象,可以序列化、可以进 MQ、可以重试、可以幂等。这些能力,策略模式一个都给不了。策略模式不会去管「这个动作执行过没有」「能不能重试」。
RocketMQ 的消息体本身就是一个命令对象。Kafka 的 record 也是。每个消息里包含**「要做什么」+ 「参数」+ 「唯一标识」**,消费端反序列化后执行。命令模式。
命令模式 vs 事件:别搞混
很多人把命令模式(Command)和观察者模式(Observer)搞混。
java
// 观察者模式:事件触发,订阅者各自响应
publisher.publish("orderCreated", orderEvent);
// 命令模式:明确指定执行某个动作
executor.execute(new SendSmsCommand(...));
观察者是「发生了什么事,感兴趣的人自己处理 」------解耦的是发布者和订阅者。 命令是「我要做这件事,帮我执行」------解耦的是动作的发起者和执行者。
观察者模式里,发布者不关心谁会处理;命令模式里,发起者明确知道要执行哪个命令。
Spring 的 ApplicationEvent 是观察者模式:发布 UserRegisteredEvent,可能 5 个 @EventListener 各自处理。
JDK 的 Runnable 是命令模式:new Thread(runnable).start(),明确执行这个任务。
CQRS:把命令模式推到极致
CQRS(Command Query Responsibility Segregation)你可能听过。它的本质就是把"改"和"查"完全分开。
java
// 写模型:所有修改都是命令
public interface Command<T> {
T execute();
}
public class CreateOrderCommand implements Command<Order> {
private final OrderDTO dto;
public Order execute() {
Order order = Order.create(dto);
orderRepository.save(order);
return order;
}
}
public class UpdateOrderCommand implements Command<Void> {
private final Long orderId;
private final OrderDTO dto;
public Void execute() {
// ... 更新逻辑
return null;
}
}
// 读模型:所有查询都走专门的 QueryHandler
public interface Query<T> {
T execute();
}
public class GetOrderListQuery implements Query<List<OrderVO>> {
private final UserId userId;
private final int page;
public List<OrderVO> execute() {
return orderReadRepository.findByUserId(userId, page);
}
}
阿里内部的交易系统、Axon Framework、EventSourcing 框架------全是这样设计的。每个用户操作对应一个 Command 对象,包含所有执行所需的参数。命令可以被序列化、可以进事件溯源日志、可以被回放、可以异步执行。
这种设计的核心好处:整个系统的所有状态变化都有完整的审计日志。出问题时,可以重放命令序列,调试"为什么这条订单会变成这个状态"。
策略模式能给这个吗?给不了。策略模式关注的是"用什么算法",不是"做了什么动作"。
命令模式的四个核心价值
总结一下,命令模式能解决策略模式解决不了的四个问题:
- 可撤销------保存执行状态,支持反向操作(撤销/重做)
- 可序列化------命令对象可保存、可传输(MQ、RPC、日志)
- 可队列化------命令可排队、按序执行、延迟执行
- 可审计------每个命令有完整上下文,可记录、可回放
策略模式解决"同一件事有几种做法,挑一个用",命令模式解决"做了一件事,要追踪它、回退它、转发它"。
代码上看,命令模式和策略模式都长 interface Xxx { void execute(); }。但接口长一样不代表意图一样。
实战中什么时候用命令模式
判断标准:
- 你的动作需要撤销/重做吗? → 命令模式
- 你的动作需要异步执行(MQ、定时任务)? → 命令模式
- 你的动作需要记录日志、审计? → 命令模式
- 你的动作需要远程传输(RPC、分布式事务)? → 命令模式
- 你只是有几种不同的算法实现,根据条件选一个? → 策略模式
举个最容易混的例子:支付路由。
java
// 错误:把支付路由做成命令模式
public class AlipayCommand { void execute() { ... } }
public class WechatPayCommand { void execute() { ... } }
// 正确:支付路由是策略模式
public interface PayStrategy {
PayResult pay(Order order);
}
public class AlipayStrategy implements PayStrategy { ... }
public class WechatPayStrategy implements PayStrategy { ... }
支付路由的本质是「选一个渠道支付」,没有撤销、没有队列、没有日志需求。这是策略模式。
但如果加上「支付失败要退款」「支付成功要发 MQ 通知」「支付记录要进审计表」,那这些「后续动作」可以做成命令:
java
public interface PaySuccessHandler {
void onPaySuccess(Order order);
}
主流程是策略,副作用是命令。两件事别混。
设计模式的命名经常误导人。命令模式听起来像"系统命令"(ls、cd),策略模式听起来像"战略"。但本质上是对代码组织的不同意图:策略是「挑一个算法」,命令是「执行一个动作」。
代码长一样不等于模式一样。下次再有人跟你说"命令模式就是策略模式",你可以让他区分下:你的 sortStrategy.sort(list) 能撤销吗?
我做的那个小程序「爪爪代码冒险记」里,命令模式那关讲的是「快递员接任务」------卡皮巴拉是快递员,每个快递单是一个命令(包含寄件人、收件人、物品),可以派发、可以查询、可以转单、退单就是 undo。比讲 redo/undo 代码好理解。还在开发,搜一下「爪爪代码冒险记」能看到。