命令模式和策略模式代码长一样——你分不清是因为你没看穿它们的本质

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 对象,包含所有执行所需的参数。命令可以被序列化、可以进事件溯源日志、可以被回放、可以异步执行。

这种设计的核心好处:整个系统的所有状态变化都有完整的审计日志。出问题时,可以重放命令序列,调试"为什么这条订单会变成这个状态"。

策略模式能给这个吗?给不了。策略模式关注的是"用什么算法",不是"做了什么动作"。

命令模式的四个核心价值

总结一下,命令模式能解决策略模式解决不了的四个问题:

  1. 可撤销------保存执行状态,支持反向操作(撤销/重做)
  2. 可序列化------命令对象可保存、可传输(MQ、RPC、日志)
  3. 可队列化------命令可排队、按序执行、延迟执行
  4. 可审计------每个命令有完整上下文,可记录、可回放

策略模式解决"同一件事有几种做法,挑一个用",命令模式解决"做了一件事,要追踪它、回退它、转发它"。

代码上看,命令模式和策略模式都长 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 代码好理解。还在开发,搜一下「爪爪代码冒险记」能看到。

相关推荐
用户298698530141 小时前
Java Word 文档样式进阶:段落与文本背景色设置完全指南
java·后端
苍何1 小时前
开源个狠活,世界杯 AI 模型竞技场!
后端
Dilee1 小时前
Spring AI 1.1.7 接入 MCP:Filesystem Server 最小 Demo
人工智能·后端
程序员小富1 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
苍何1 小时前
深度测评 MiniMax M3,能打但不贵
后端
苍何1 小时前
爆款博主,已经没有秘密了。。。
后端
dunky1 小时前
Spring 的三级缓存与循环依赖
后端·spring
子兮曰1 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端