# 命令模式
# 现实案例
- 现在我们要实现一个智能遥控器;
- 遥控器有七个可编程插槽,每个插槽可以指定到一个智能家电;
- 每个插槽有对应的开关按钮,并且遥控器还有一个全局的撤销按钮;
- 智能家居厂商会提供所有家居控制的 API;
- 我们需要创建一组遥控器的 API,可以设置插槽所对应的电器;
- 并且 API 能够适配任何未来会出现的电器;
# 命令模式
# 简单举例说明
🌰 下面通过拿 “顾客”,“服务员”,“厨师” 来举例讲解,什么是命令模式:
- 顾客向服务员点餐;
- 服务员有个
takeOrder
方法,可以根据用户点的餐,创建一个订单; - 订单上有个
orderUp
方法,里面封装了制作餐点所需的动作。这些动作会由厨师来执行; - 服务员调用
orderUp
方法,然后厨师执行内部的过程,饭就做好了; - 通过这种方式,服务员与厨师彻底的解耦;
- 女服务员的订单封装了制作餐点的细节;
- 服务员不需要知道如何制作。他只要把订单给厨师,厨师会去制作餐点;
- 而厨师也不需要知道,到底是谁点的餐;
# 定义命令模式
- 在命令模式中,会有一个 “请求者”,它会发出一个请求,让指定的 “接收者” 去执行一些动作;
- 发出去的 “请求” 会向外暴露出一个 “执行” (execute)方法。用户需要一个 “调用者” 去调用执行方法;
- 当 “调用者” 调用了请求的 execute 方法后,“接收者” 就会进行请求中所要求的那些动作;
- 从外部来看,其他对象并不知道,究竟最后是谁执行了所有动作。只知道有人调用了
execute
方法; - 命令模式,使得 “发出请求的对象” 和 “接收和执行请求的对象” 分隔开来;
# 实现命令模式
- 首先我们创建 “命令” 接口。接口中定义
execute
方法; - 所有的命令子类,都需要实现
execute
方法; - 方法中需要定义让 “谁” 去执行什么 “动作”;
public interface Command {
public void execute();
}
- 接下来开始实现 “命令类”;
- 命令类里面需要有一个 “接收者” 类型的实例属性。作为接收者的引用;
- 在实例化时,把这个接收者对象传入;
execute
里面的动作,需要让 “接收者” 来执行;
public class ConcreteCommand implements Command {
Receiver receiver;
public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
public void execute() {
receiver.action();
}
}
- 现在来定义 “调用者”,它最后会去调用 “命令对象” 的
execute
方法;
public class Invoker {
Command command;
public void setCommand(Command command) {
this.command = command;
}
public void on() {
command.execute();
}
}
- 下面来写使用命令模式;
- Client 是命令模式的客户;
- 在创建命令对象时,传入接收者;
- 然后在把命令对象传入调用者;
- 最后调用者会进行调用,接收者执行命令对象里封装的动作;
public class Client {
public static void main(String[] args) {
Invoker invoker = new Invoker();
Receiver receiver = new Receiver();
ConcreteCommand concreteCommand = new ConcreteCommand(receiver);
invoker.setCommand(concreteCommand);
invoker.on();
}
}
# 实现智能遥控器
下面开始用 “命令模式” 实现智能遥控器。
整体思路是:
- 整个遥控器是一个 “客户”;
- 七个插槽上的每个开关,对应是 “调用者”。每个按钮绑定上一个命令对象。每按下一个按钮,对应的命令对象的
execute
方法就会被执行; - 智能家居类就是对应的 “接收者”。命令对象的
execute
方法内会封装对这些家具 API 的调用;
- 先实现命令接口;
public interface Command {
public void execute();
}
- 下面实现一个 “开灯” 功能的命令类;
- Light 类由家具厂商提供;
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on(); // 点开灯
}
}
- 现在来实现遥控器的代码;
public class RemoteControl {
Command[] onCommands;
Command[] offCommands;
Command undoCommand;
public RemoteControl() {
onCommands = new Command[7];
offCommands = new Command[7];
// 最开始的时候,每个插槽设置的是 “无命令” 命令对象;
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
undoCommand = noCommand;
}
// 因为每个插槽,有一个 on 开关,和一个 off 开关;
// 所以我们一次设置两个命令对象;
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
// 按下按钮,执行对应的命令对象;
public void onButtowWasPushed(int slot) {
onCommands[slot].execute();
undoCommand = onCommands[slot];
}
public void offButtowWasPushed(int slot) {
offCommands[slot].execute();
undoCommand = offCommands[slot];
}
}
# 命令模式的更多用途
# 队列请求
- 命令可以将运算块打包(一个接受者和一组动作),然后将它传来传去;
- 即使在命令对象被创建许久之后,运算依然可以被调用,事实上,它甚至可以在不同的线程中被调用;
- 我们可以利用这样的特性衍生一些应用,例如:日程安排,线程池,工作队列等;
- 想象有一个工作队列:你在某一端添加命令,然后另一端则是线程;
- 线程进行下面的动作:从队列中取出一个命令,调用它的
execute()
方法; - 等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令;
- 这种方式,使得工作队列和进行计算的对象之间是完全解耦的;
# 日志请求
- 某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的状态;
- 通过新增两个方法(store、 load),命令模式就能够支持这一点;
- 当我们执行命令的时候,将历史记录储存在磁盘中。一旦系统死机,我们就可以将命令对象重新加载,并成批地依次调用这些对象的
execute
方法; - 有许多调用大型数据结构的动作的应用,无法在每次改变发生时被快速地存储;
- 通过使用记录日志,我们可以将上次检查点(checkpoint)之后的所有操作记录下来;
- 如果系统出状况,从检查点开始应用这些操作;
- 比方说,对于电子表格应用,我们可能想要实现的错误恢复方式是将电子表格的操作记录在日志中;
- 而不是每次电子表格一有变化就记录整个电子表格;