GoF设计模式——命令模式
为什么需要命令模式想象一个文本编辑器的撤销功能用户输入了一段文字然后按 CtrlZ 撤销。最直觉的写法是在每个操作方法里保存历史状态class TextEditor { private StringBuilder content new StringBuilder(); private ListString history new ArrayList(); public void insertText(String text, int position) { history.add(content.toString()); // 保存当前状态 content.insert(position, text); } public void undo() { if (!history.isEmpty()) { content new StringBuilder(history.remove(history.size() - 1)); } } }这种写法很快就会失控每加一种操作删除、替换、格式化就要在对应方法里手动保存历史TextEditor既要管业务逻辑又要管撤销状态职责混乱。操作一多历史管理代码散落在各处谁也不敢碰这块代码。命令模式解决的就是这个把操作本身变成可以存储、排队、撤销的对象的问题。每个操作封装成一个命令对象编辑器只管持有命令、在合适时调用需要撤销就调用命令的undo()方法互不干扰。概念命令模式Command Pattern是一种行为型设计模式核心思想是将请求封装为对象从而使你可以用不同的请求对客户进行参数化支持请求的排队、记录日志以及撤销操作。命令模式的灵魂是把请求变成对象——可以存储、排队、撤销、组合。Command 代表一个可以被存储、排队、撤销的操作。命令模式包含四个角色Command抽象命令类声明执行操作的接口通常包含execute()方法ConcreteCommand具体命令类将一个接收者对象绑定于一个动作实现execute()方法调用接收者的相应操作Receiver接收者真正执行请求的对象知道如何实施与执行一个请求相关的操作Invoker调用者持有命令对象并调用其execute()方法发送请求实现持有持有«interface»Commandexecute()ConcreteCommand-receiver: Receiverexecute()Receiveraction()Invoker-command: CommandsetCommand(command)executeCommand()图中各类之间的关系ConcreteCommand实现Command接口并持有Receiver引用Invoker持有一个Command引用——Invoker和Receiver之间没有直接依赖。可以把命令模式想象成餐厅点菜顾客客户端跟服务员Invoker说来一份宫保鸡丁服务员不自己下厨而是把需求写成订单Command转交给后厨厨师Receiver。订单可以排队、可以取消、可以记录服务员和厨师完全解耦。Invoker 负责调度什么时候做Command 负责绑定把谁做和做什么绑在一起。Invoker 不该知道 Receiver就像调度员不该替老板决定谁干活。如何区分哪些操作是命令如果一个操作需要满足以下任一条件就应该考虑将其设计为命令需要撤销/重做操作需要记录历史状态支持回滚需要排队执行操作需要按顺序执行或延迟执行需要事务支持多个操作需要作为一个原子单元执行需要日志记录操作需要被记录以便审计或恢复需要解耦发送者和接收者发送者不应该知道接收者的具体类型实现标准实现GoF 推荐 Receiver 由客户端创建并注入到具体命令中原因如下职责分离客户端知道业务上下文能决定使用哪个接收者调用者只负责调度不应该知道业务细节灵活性同一个命令可以配合不同的接收者使用如保存命令可以保存到文件或数据库可测试性便于单元测试时注入 Mock 接收者下面是标准实现的代码示例由客户端创建 Receiver 并注入到具体命令中// 抽象命令类 public interface Command { public void execute(); } // 具体命令类 public class ConcreteCommand implements Command { private Receiver receiver; public ConcreteCommand(Receiver receiver) { this.receiver receiver; } public void execute() { receiver.action(); } } // 接收者 public class Receiver { public void action() { System.out.println(Receiver action executed); } } // 调用者 public class Invoker { private Command command; public void setCommand(Command command) { this.command command; } public void executeCommand() { command.execute(); } } // 客户端代码 Receiver receiver new Receiver(); Command command new ConcreteCommand(receiver); Invoker invoker new Invoker(); invoker.setCommand(command); invoker.executeCommand();⚠️反模式如果由调用者Invoker创建接收者会导致调用者与具体业务耦合违背了命令模式的初衷。正确做法是由客户端创建 Receiver 并注入到具体命令中。命令队列延迟执行命令队列用于延迟执行场景——命令先入队之后统一执行。核心原则队列管还没做的入队的命令尚未执行从队列移除只是取消不需要回滚。// Command 接口只需 execute() public interface Command { public void execute(); } // 命令队列管理未执行的命令 class CommandQueue { private DequeCommand queue new ArrayDeque(); public void addCommand(Command command) { queue.offer(command); } public void executeAll() { while (!queue.isEmpty()) { queue.poll().execute(); } } public void cancelLast() { // 从队尾移除最后一个未执行的命令还没执行所以是取消不是撤销 if (!queue.isEmpty()) { queue.pollLast(); } } }撤销栈执行后可回滚撤销栈用于需要回滚的场景——命令先执行再压入历史栈需要时可以撤销。核心原则撤销栈管已经做的每个命令必须实现undo()方法以支持回滚。// Command 接口需同时声明 execute() 和 undo() public interface UndoableCommand { public void execute(); public void undo(); } // 撤销栈管理已执行的命令支持回滚 class CommandHistory { private DequeUndoableCommand history new ArrayDeque(); public void execute(UndoableCommand command) { command.execute(); history.push(command); } public void undo() { if (!history.isEmpty()) { history.pop().undo(); } } }一句话区分队列管还没做的取消即可撤销栈管已经做的需要回滚。不要把两者混在一个类里。常见反模式以下反模式基于一个典型场景展开点餐系统中用户依次添加订单命令到队列如奶茶→咖啡→果汁Cancel 是取消队列中最后一个未执行的命令从队列移除Confirm 是确认并执行队列中所有命令按顺序制作。Cancel 和 Confirm 都是对队列本身的管理操作。反模式一不是所有操作都适合做成 CommandCancel 和 Confirm 是 Invoker 对队列的管理操作属于 Invoker 的职责不应设计为 Command。只有需要被存储、排队、撤销的业务操作才是 Command。// ❌ 错误CancelCommand 放进队列confirm 时会执行它——毫无意义 q.addLast(new OrderCommand(MilkTea)); q.addLast(new CancelCommand()); // confirm 时执行 CancelCommand语义矛盾 // ✅ 正确Cancel 是 Invoker 提供的能力直接操作队列 q.removeLast(); // 从队列移除不涉及任何 Command 执行反模式二只有一种 Receiver 时Receiver 是多余的一层Receiver 的价值在于同一个 Command 接口注入不同 Receiver 产生不同行为如SaveCommand可以注入 FileReceiver 存文件也可以注入 DbReceiver 存数据库。如果只有一种 Receiver去掉它直接在 Command 里写逻辑没有区别反而增加了一个无意义的间接层。总结命令模式的本质是把请求封装成对象让请求可以被存储、排队、撤销、组合——发送者和接收者完全解耦命令对象成为两者之间的桥梁。什么时候用系统需要支持撤销/重做功能需要将操作排队、延迟执行或批处理需要记录操作日志以便审计或恢复需要支持事务性操作多个操作要么全部成功要么全部回滚发送者和接收者需要解耦什么时候不用简单的请求调用不需要撤销、排队、日志等功能只有一种 ReceiverReceiver 层没有存在的必要操作不需要被存储或传递简单记忆命令模式解决把请求变成对象的问题让操作可以存储、排队、撤销、组合。⚠️ 用命令模式要注意每个具体命令都需要一个类类数量会爆炸如果命令需要支持撤销需要维护命令执行前的状态增加了实现复杂度。相似模式区分命令模式容易和策略、状态模式混淆它们都涉及封装行为但实现方式和意图不同。总览对比模式核心意图典型场景命令将请求封装为对象支持排队、撤销、日志撤销重做、事务处理、任务队列策略定义算法族使它们可以相互替换折扣算法、支付方式、排序状态允许对象在状态改变时改变其行为订单状态流转、审批流程口诀命令管请求策略管算法状态管行为。命令 vs 策略两者都涉及封装可变行为但意图完全不同。命令模式关注的是请求的管理——把请求变成对象支持存储、排队、撤销策略模式关注的是算法的替换——把算法封装成独立对象运行时可互换。维度命令模式策略模式核心意图将请求封装为对象支持排队、撤销、日志定义算法族使它们可以相互替换结构差异包含 Command、Receiver、Invoker包含 Strategy、Context关注点请求的封装和管理算法的替换和扩展典型场景撤销重做、事务处理、任务队列排序算法、支付方式、折扣策略逐步区分法如果需要支持撤销、重做、排队 → 选择命令模式如果需要动态切换算法或策略 → 选择策略模式如果需要将请求发送者与接收者解耦 → 选择命令模式如果需要让算法独立于使用它的客户端变化 → 选择策略模式简单记忆口诀命令管请求策略管算法。命令 vs 状态两者都涉及封装行为但实现方式和意图完全不同。命令模式中命令对象是独立的可以被存储、传递状态模式中状态对象依赖于 Context状态之间可以触发转换。维度命令模式状态模式核心意图将请求封装为对象允许对象在状态改变时改变其行为结构差异Command 对象是独立的State 对象依赖于 Context关注点请求的封装和管理对象状态的转换和行为典型场景撤销重做、事务处理订单状态、工作流状态逐步区分法如果需要将请求封装为对象以便管理 → 选择命令模式如果对象行为随状态改变而改变 → 选择状态模式如果需要支持撤销和重做 → 选择命令模式如果需要管理对象的生命周期状态 → 选择状态模式简单记忆口诀命令封装请求状态管理行为。练习题目自助点餐机题目描述奶茶店的自助点餐机支持堂食和外卖两种订单。堂食订单由堂食接收者处理输出Dine-in: XXX is ready!外卖订单由外卖接收者处理输出Takeout: XXX is ready!。点餐机支持点单、取消、确认三种操作。输入描述第一行是一个整数 n1 ≤ n ≤ 100表示操作的数量。接下来的 n 行每行包含操作信息点单操作1 饮品名称 订单类型D 表示堂食T 表示外卖取消操作2确认操作3保证取消操作时队列不为空确认操作时队列不为空。输出描述每次确认操作时按顺序输出队列中所有订单的制作情况。输入示例8 1 MilkTea D 1 Coffee T 1 Cola D 1 Juice T 2 3 1 Coffee D 3输出示例Dine-in: MilkTea is ready! Takeout: Coffee is ready! Dine-in: Cola is ready! Dine-in: Coffee is ready!设计推演看题目描述提取关键信息输入点单操作饮品名 堂食/外卖、取消、确认输出确认时按顺序输出所有订单的制作情况关键词按顺序输出、队列、取消订单不是点一个做一个而是先攒着确认时才统一执行——这是命令队列的典型场景。识别角色CommandMakeCommand封装制作请求做什么饮品 谁来做ReceiverDineinReceiver和TakeoutReceiver真正执行制作的人InvokerOrderSystem只管命令队列点单、取消、确认不关心怎么做饮品为什么 Invoker 不直接持有 Receiver因为职责混乱、难以扩展。正确做法客户端负责组装决定谁做Invoker 只负责调度什么时候做Command 负责绑定把谁做和做什么绑在一起。解题思路Command 是点单命令Receiver 是做外卖的员工和做堂食的员工Invoker 是点单系统。点单命令创建后加入队列确认时按顺序执行延迟执行不是立即制作。客户端负责创建 Receiver 实例并注入到具体命令中Invoker 只管理命令队列不关心具体业务逻辑。import java.util.*; public class Main { public static void main(String[] args) { Scanner sc new Scanner(System.in); int n sc.nextInt(); // Client 创建 Receiver 实例 Receiver dineIn new DineinReceiver(); Receiver takeout new TakeoutReceiver(); // Invoker 只管命令队列 OrderSystem os new OrderSystem(); while (n-- 0) { int op sc.nextInt(); if (op 1) { String name sc.next(); String type sc.next(); // Client 创建 Command 并指定 Receiver Receiver r D.equals(type) ? dineIn : takeout; Command cmd new MakeCommand(name, r); os.order(cmd); } else if (op 2) { os.cancel(); } else { os.confirm(); } } } } // Command声明接口 interface Command { public void execute(); } // ConcreteCommand绑定 Receiver 和动作 class MakeCommand implements Command { private String drinkName; private Receiver receiver; public MakeCommand(String drinkName, Receiver receiver) { this.drinkName drinkName; this.receiver receiver; } public void execute() { receiver.make(drinkName); } } // Invoker只管理命令队列只和 Command 接口交互 class OrderSystem { private DequeCommand q new ArrayDeque(); public void order(Command command) { q.addLast(command); } public void cancel() { q.removeLast(); } public void confirm() { while (!q.isEmpty()) { q.pollFirst().execute(); } } } // Receiver 接口 interface Receiver { public void make(String name); } // ConcreteReceiver class DineinReceiver implements Receiver { public void make(String name) { System.out.println(Dine-in: name is ready!); } } class TakeoutReceiver implements Receiver { public void make(String name) { System.out.println(Takeout: name is ready!); } }扩展实际项目中的命令模式撤销/重做文本编辑器文本编辑器需要支持撤销操作命令模式可以将每个操作封装为命令对象通过记录命令历史实现撤销和重做。// 抽象命令支持撤销 public interface UndoableCommand { public void execute(); public void undo(); } // 具体命令插入文本 public class InsertTextCommand implements UndoableCommand { private TextEditor editor; private String text; private int position; public InsertTextCommand(TextEditor editor, String text, int position) { this.editor editor; this.text text; this.position position; } public void execute() { editor.insertText(text, position); } public void undo() { editor.deleteText(position, position text.length()); } } // 调用者编辑器控制器维护命令历史 public class EditorController { private ListUndoableCommand history new ArrayList(); private int currentCommandIndex -1; public void executeCommand(UndoableCommand command) { command.execute(); history new ArrayList(history.subList(0, currentCommandIndex 1)); history.add(command); currentCommandIndex; } public void undo() { if (currentCommandIndex 0) { history.get(currentCommandIndex).undo(); currentCommandIndex--; } } public void redo() { if (currentCommandIndex history.size() - 1) { currentCommandIndex; history.get(currentCommandIndex).execute(); } } }关键点插入命令撤销时执行反操作删除即可命令历史列表支持重做功能需要维护当前命令索引执行新命令时需要清除索引之后的所有命令。类似的场景数据库事务管理转账时扣款和加款要么都成功要么都回滚、图形编辑器的宏命令多个操作组合成一个命令撤销时逆序撤销所有子命令。命令队列任务调度在电商系统中订单处理需要执行多个步骤库存扣减、支付处理、物流通知等这些步骤可以异步执行。命令模式可以将每个步骤封装为命令对象由任务队列统一调度。// 抽象命令 public interface TaskCommand { public void execute(); public String getTaskName(); } // 具体命令库存扣减 public class ReduceInventoryCommand implements TaskCommand { private InventoryService inventoryService; private String productId; private int quantity; public ReduceInventoryCommand(InventoryService inventoryService, String productId, int quantity) { this.inventoryService inventoryService; this.productId productId; this.quantity quantity; } public void execute() { inventoryService.reduce(productId, quantity); } public String getTaskName() { return Reduce inventory for product: productId; } } // 调用者任务队列异步执行 public class TaskQueue { private QueueTaskCommand queue new LinkedList(); private ExecutorService executor Executors.newFixedThreadPool(3); public void addTask(TaskCommand command) { queue.offer(command); } public void processTasks() { while (!queue.isEmpty()) { TaskCommand command queue.poll(); executor.submit(() - { try { command.execute(); System.out.println(Task completed: command.getTaskName()); } catch (Exception e) { System.err.println(Task failed: command.getTaskName()); } }); } } public void shutdown() { executor.shutdown(); }