本文是【GoF计划模式】系列第15篇,更多内容接待关注公众号:咖啡八杯
媒介
为什么须要下令模式?
想象一个文本编辑器的取消功能:用户输入了一段笔墨,然后按 Ctrl+Z 取消。最直觉的写法是在每个操纵方法里生存汗青状态:- class TextEditor {
- private StringBuilder content = new StringBuilder();
- private List<String> 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() 方法发送哀求
classDiagram direction BT class Command { +execute() } class ConcreteCommand { -receiver: Receiver +execute() } class Receiver { +action() } class Invoker { -command: Command +setCommand(command) +executeCommand() } ConcreteCommand ..|> Command : 实现 ConcreteCommand o--> Receiver : 持有 Invoker o--> Command : 持有图中各类之间的关系: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 Deque<Command> 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 Deque<UndoableCommand> 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 都是对队列自己的管理操纵。
反模式一:不是全部操纵都得当做成 Command
Cancel 和 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 里写逻辑没有区别,反而增长了一个偶然义的间接层。
总结
下令模式的本质是把哀求封装成对象,让哀求可以被存储、列队、取消、组合——发送者和吸取者完全解耦,下令对象成为两者之间的桥梁。
什么时间用:
- 体系须要支持取消/重做功能
- 须要将操纵列队、耽误实验或批处置惩罚
- 须要纪录操纵日记以便审计或规复
- 须要支持事件性操纵(多个操纵要么全部乐成,要么全部回滚)
- 发送者和吸取者须要解耦
什么时间不消:
- 简单的哀求调用,不须要取消、列队、日记等功能
- 只有一种 Receiver,Receiver 层没有存在的须要
- 操纵不须要被存储或转达
简单影象:
下令模式办理"把哀求变成对象"的题目,让操纵可以存储、列队、取消、组合。
⚠️ 用下令模式要留意:每个详细下令都须要一个类,类数目会爆炸;如果下令须要支持取消,须要维护下令实验前的状态,增长了实现复杂度。
相似模式区分
下令模式轻易和战略、状态模式肴杂,它们都涉及"封装运动",但实现方式和意图差别。
总览对比:
模式核心意图典范场景下令将哀求封装为对象,支持列队、取消、日记取消重做、事件处置惩罚、使命队列战略界说算法族,使它们可以相互更换扣头算法、付出方式、排序状态允许对象在状态改变时改变其运动订单状态流转、审批流程口诀:下令管哀求,战略管算法,状态管运动。
下令 vs 战略
两者都涉及"封装可变运动",但意图完全差别。下令模式关注的是哀求的管理——把哀求变成对象,支持存储、列队、取消;战略模式关注的是算法的更换——把算法封装成独立对象,运行时可交换。
维度下令模式战略模式核心意图将哀求封装为对象,支持列队、取消、日记界说算法族,使它们可以相互更换布局差别包罗 Command、Receiver、Invoker包罗 Strategy、Context关注点哀求的封装和管理算法的更换和扩展典范场景取消重做、事件处置惩罚、使命队列排序算法、付出方式、扣头战略渐渐区分法:
- 如果须要支持取消、重做、列队 → 选择下令模式
- 如果须要动态切换算法或战略 → 选择战略模式
- 如果须要将哀求发送者与吸取者解耦 → 选择下令模式
- 如果须要让算法独立于使用它的客户端变革 → 选择战略模式
简单影象口诀:下令管哀求,战略管算法。
下令 vs 状态
两者都涉及"封装运动",但实现方式和意图完全差别。下令模式中,下令对象是独立的,可以被存储、转达;状态模式中,状态对象依靠于 Context,状态之间可以触发转换。
维度下令模式状态模式核心意图将哀求封装为对象允许对象在状态改变时改变其运动布局差别Command 对象是独立的State 对象依靠于 Context关注点哀求的封装和管理对象状态的转换和运动典范场景取消重做、事件处置惩罚订单状态、工作流状态渐渐区分法:
- 如果须要将哀求封装为对象以便管理 → 选择下令模式
- 如果对象运动随状态改变而改变 → 选择状态模式
- 如果须要支持取消和重做 → 选择下令模式
- 如果须要管理对象的生命周期状态 → 选择状态模式
简单影象口诀:下令封装哀求,状态管理运动。
训练标题
自助点餐机
标题形貌:奶茶店的自助点餐机支持堂食和外卖两种订单。堂食订单由堂食吸取者处置惩罚,输出 Dine-in: XXX is ready!;外卖订单由外卖吸取者处置惩罚,输出 Takeout: XXX is ready!。点餐机支持点单、取消、确认三种操纵。
输入形貌:第一行是一个整数 n(1 ≤ 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!
复制代码 计划推演:
看标题形貌,提取关键信息:
- 输入:点单操纵(饮品名 + 堂食/外卖)、取消、确认
- 输出:确认时按次序输出全部订单的制作环境
- 关键词:按次序输出、队列、取消
订单不是点一个做一个,而是先攒着,确认时才同一实验——这是下令队列的典范场景。
辨认脚色:
- Command:MakeCommand,封装"制作哀求"(做什么饮品 + 谁来做)
- Receiver:DineinReceiver 和 TakeoutReceiver,真正实验制作的人
- Invoker:OrderSystem,只管下令队列(点单、取消、确认),不关心怎么做饮品
为什么 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 Deque<Command> 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 List<UndoableCommand> 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 Queue<TaskCommand> 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();
- }
- }
复制代码 关键点:使命队列将下令存储在队列中,由线程池异步实验;每个下令都是独立的,可以并行实验;使命队列不关心详细业务逻辑,只负责调治实验。
类似的场景:GUI 按钮变乱处置惩罚(按钮不直接调用文档方法,而是通过下令对象解耦,支持运行时动态更换按钮功能)。
技能交换 & 更多原创内容,关注公众号:咖啡八杯
免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金. |