核心思想
- 命令模式(Command Pattern)是一种行为型设计模式,将请求(操作)封装成一个独立对象,从而解耦请求方与接收方,使得命令的执行、撤销、排队等操作更易扩展和灵活。
结构
1. Command(命令接口)
- 定义一个执行命令的接口,所有具体命令类都需实现此接口。
2. ConcretCommand(具体命令)
- 实现 Command 接口,定义与接收者之间的绑定关系,负责调用接收者的相应操作方法。
3. Receiver(实现者/接收者)
- 执行真正的命令(业务逻辑),任何类都能成为接收者,只要它能实现命令要求实现的功能。
4. Invoker(调用者/请求者)
- 持有命令对象,负责在适当时刻调用命令的
execute()
方法,且不需要知道命令的具体实现细节。
现实世界类比
适用场景
1. 需要通过操作来参数化对象:
- 命令模式可将特定的方法调用转化为独立对象。 这一改变也带来了许多有趣的应用: 你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。
- 举个例子: 你正在开发一个 GUI 组件 (例如上下文菜单), 你希望用户能够配置菜单项, 并在点击菜单项时触发操作。
2. 需要将操作放入队列、操作的执行或远程执行操作:
- 同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。
- 示例:
import java.util.ArrayList; import java.util.List; // 命令接口 interface Command { void execute(); } // 具体命令:发送通知 class SendNotificationCommand implements Command { private String message; public SendNotificationCommand(String message) { this.message = message; } @Override public void execute() { System.out.println("发送通知: " + message); } } // 任务调度器(支持延迟执行) class TaskScheduler { private List<Command> taskQueue = new ArrayList<>(); public void addTask(Command command) { taskQueue.add(command); } public void executeAll() { for (Command command : taskQueue) { command.execute(); } taskQueue.clear(); } } // 测试代码 public class CommandPatternDelayedDemo { public static void main(String[] args) throws InterruptedException { TaskScheduler scheduler = new TaskScheduler(); // 添加任务,但不立即执行 scheduler.addTask(new SendNotificationCommand("任务 1")); scheduler.addTask(new SendNotificationCommand("任务 2")); // 模拟延迟 System.out.println("任务将在 3 秒后执行..."); Thread.sleep(3000); // 现在执行所有任务 scheduler.executeAll(); } }
3. 需要实现回滚功能:
- 尽管有很多方法可以实现撤销和恢复功能, 但命令模式可能是其中最常用的一种。
- 为了能够回滚操作, 你需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。
- 这种方法有两个缺点。 首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。 你可以使用备忘录模式来在一定程度上解决这个问题。
- 其次, 备份状态可能会占用大量内存。 因此, 有时你需要借助另一种实现方式: 命令无需恢复原始状态, 而是执行反向操作。 反向操作也有代价: 它可能会很难甚至是无法实现。
- 示例:
import java.util.Stack; // 命令接口 interface Command { void execute(); // 执行命令 void undo(); // 撤销命令 } // 具体命令:写入文本 class WriteCommand implements Command { private StringBuilder document; private String text; public WriteCommand(StringBuilder document, String text) { this.document = document; this.text = text; } @Override public void execute() { document.append(text); } @Override public void undo() { document.delete(document.length() - text.length(), document.length()); } } // 命令管理者(支持撤销/恢复) class CommandManager { private Stack<Command> history = new Stack<>(); public void executeCommand(Command command) { command.execute(); history.push(command); } public void undoCommand() { if (!history.isEmpty()) { history.pop().undo(); } } } // 测试代码 public class CommandPatternUndoDemo { public static void main(String[] args) { StringBuilder document = new StringBuilder(); CommandManager manager = new CommandManager(); // 写入 "Hello " Command cmd1 = new WriteCommand(document, "Hello "); manager.executeCommand(cmd1); System.out.println(document); // 输出: Hello // 写入 "World!" Command cmd2 = new WriteCommand(document, "World!"); manager.executeCommand(cmd2); System.out.println(document); // 输出: Hello World! // 撤销最近的写入 manager.undoCommand(); System.out.println(document); // 输出: Hello // 再次撤销 manager.undoCommand(); System.out.println(document); // 输出: (空字符串) } }
4. 需要解耦调用者与接收者
优点:
1. 遵循单一职责原则:
- 解耦调用者与接收者。
2. 遵循开闭原则:
- 在不修改客户端代码的前提下创建新的命令。
3. 命令组合:
- 多个命令组成一个宏命令,来调用更多操作。
4. 可实现撤销、恢复功能:
- 在命令模式中,每个操作都会封装成一个命令对象,这样我们可以将命令存入一个历史记录栈,然后在需要撤销时回滚。
5. 可实现操作的延迟执行:
- 命令模式将操作封装成一个对象,这个对象可以存储、传输、排队,并在需要时再执行。
缺点:
1. 增加复杂性
2. 类数量增加
实现步骤
- 声明仅有一个执行方法的命令接口。
- 抽取请求并使之成为实现命令接口的具体命令类。 每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。 所有这些变量的数值都必须通过命令构造函数进行初始化。
- 找到担任调用者职责的类。 在这些类中添加保存命令的成员变量。 发送者只能通过命令接口与其命令进行交互。 发送者自身通常并不创建命令对象, 而是通过客户端代码获取。
- 修改发送者使其执行命令, 而非直接将请求发送给接收者。
- 客户端必须按照以下顺序来初始化对象:
- 创建接收者。
- 创建命令, 如有需要可将其关联至接收者。
- 创建发送者并将其与特定命令关联。
示例
// 订单类 public class Order { private int diningTable; private Map<String, Integer> foodDir = new HashMap<>(); public int getDiningTable() { return diningTable; } public void setDiningTable(int diningTable) { this.diningTable = diningTable; } public Map<String, Integer> getFoodDic() { return foodDir; } public void setFood(String foodName, int num) { this.foodDir.put(foodName, num); } } // 接收者——厨师 public class SeniorChef { public void makeFood(String foodName, int num){ System.out.println(num + "份" + foodName); } } // 命令接口 public interface Command { void execute(); } // 具体命令——点餐命令 public class OrderCommand implements Command{ private SeniorChef receiver; private Order order; public OrderCommand(SeniorChef receiver, Order order) { this.receiver = receiver; this.order = order; } @Override public void execute() { System.out.println("点餐命令:" + order.getDiningTable() + "桌点的菜单:"); Map<String, Integer> foodDic = order.getFoodDic(); if (foodDic != null) { for (Map.Entry<String, Integer> entry : foodDic.entrySet()){ receiver.makeFood(entry.getKey(), entry.getValue()); } } System.out.println(order.getDiningTable() + "桌点餐完成!"); } } // 调用者——服务员 public class Waiter { private ArrayList<Command> commands = new ArrayList<>(); public void setCommand(Command cmd) { commands.add(cmd); } public void orderUp(){ System.out.println("美女服务员:客人点菜完毕,开始通知厨子做菜-----"); for (Command command : commands) { if (command != null) { command.execute(); } } } } // 测试类 public class Client { public static void main(String[] args) { // 创建一个调用者对象——服务员 Waiter invoke = new Waiter(); // 创建1号订单 Order order1 = new Order(); order1.setDiningTable(1); order1.setFood("西红柿鸡蛋面", 2); order1.setFood("宫保鸡丁", 1); // 创建2号订单 Order order2 = new Order(); order2.setDiningTable(2); order2.setFood("板面", 2); order2.setFood("九转大肠", 1); // 创建一个接收者对象——厨师 SeniorChef receiver = new SeniorChef(); // 创建一个命令对象,让同一个厨师做2个订单 OrderCommand orderCommand1 = new OrderCommand(receiver, order1); OrderCommand orderCommand2 = new OrderCommand(receiver, order2); // 设置命令 invoke.setCommand(orderCommand1); invoke.setCommand(orderCommand2); // 点菜 invoke.orderUp(); } }
在源码中的应用
// Runnable为命令接口 // 具体命令:实现了 Runnable 接口,封装了一个具体操作 class LightOnCommand implements Runnable { private Light light; public LightOnCommand(Light light) { this.light = light; } @Override public void run() { light.turnOn(); } } // 具体命令:实现了 Runnable 接口,封装了一个具体操作 class LightOffCommand implements Runnable { private Light light; public LightOffCommand(Light light) { this.light = light; } @Override public void run() { light.turnOff(); } } // 接收者:负责具体操作的类 class Light { public void turnOn() { System.out.println("The light is ON"); } public void turnOff() { System.out.println("The light is OFF"); } } // 客户端:创建命令并将其传递给调用者 public class CommandPatternExample { public static void main(String[] args) { // 创建接收者 Light light = new Light(); // 创建命令(Runnable 实现) Runnable lightOn = new LightOnCommand(light); Runnable lightOff = new LightOffCommand(light); // 调用者(线程)执行命令 Thread thread1 = new Thread(lightOn); thread1.start(); Thread thread2 = new Thread(lightOff); thread2.start(); } }
与其他模式的关系
- 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
- 命令在发送者和请求者之间建立单向连接。
- 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
- 观察者允许接收者动态地订阅或取消接收请求。
- 责任链的管理者可使用命令模式实现。 在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。
- 还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。
- 你可以同时使用命令和备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。
- 命令和策略模式看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。
- 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。
- 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。