命令模式
把方法调用(method invocation)封装起来。通过封装方法调用,把运算块包装成型,调用该运算的对象不需要关心事情如何进行,只需要知道如何使用该方法来完成它即可。
需求
设计一个家电自动化遥控器的API,原型遥控器具有七个可编程的插槽(每个都可以指定到一个不同的家电装置),每个插槽都有对应的开关按钮。该遥控器还具备一个整体的撤销按钮。
已有资源:一组java类,由多家厂商开发的,用来控制家电自动化装置,如电灯、风扇、热水器、音响设备和其他类似的可控制装置。
餐厅的工作流程
- 顾客知道自己要的是什么,并创建一张订单,createOrder(),订单包含一个订单表格,顾客订购的餐点项目写在上面
- 女招待拿走订单,放在订单柜台,takeOrder(),然后调用orderUp()方法,通知厨师开始准备餐点
- 厨师根据订单上的指令,如makeBurger(),makeShake()准备餐点,然后outPut()
餐厅的角色和职责
- 订单封装了准备餐点的请求
- 女招待的工作是接受订单,然后调用订单的orderUp()方法
- 厨师具备准备餐点的知识
从餐厅到命令模式
- 客户创建一个命令对象 createCommandObject()
- 客户利用setCommand()将命令对象储存在调用者中
- 稍后,客户要求调用者执行命令。一旦命令被加载到调用者,该命令可以被使用并丢弃,或可以被保留并使用多次。
餐厅和命令模式的对应关系:
- 顾客 client
- 订单 Command
- OrderUp() execute()
- 女招待 Invoker
- takeOrder() setCommand()
- 快餐厨师 Receiver
遥控器
public interface Command {
void execute();
}
public class Light {
public Light() {
}
public void on(){
System.out.println("light is on");
}
public void off(){
System.out.println("light is off");
}
}
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
public class RemoteController {
Command slot; // 插槽
public RemoteController() {
}
public void setCommand(Command command){
slot = command;
}
public void buttonPressed(){
slot.execute();
}
}
public class RemoteControllerTest {
public static void main(String[] args) {
RemoteController remoteController = new RemoteController();
Light light = new Light();
LightOnCommand lightOnCommand = new LightOnCommand(light);
GarageDoor garageDoor = new GarageDoor();
GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
remoteController.setCommand(lightOnCommand);
remoteController.buttonPressed();
System.out.println("===============");
remoteController.setCommand(garageDoorOpenCommand);
remoteController.buttonPressed();
}
}
命令模式的定义
命令模式将“请求”封装为对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
仔细看该定义,一个命令对象通过在特定接收者上绑定一组动作来封装一个请求。要达到这一点,命令对象将动作和接收者包进对象中。这个对象只暴露一个execute()方法,当此方法被调用,接收者会进行这些动作。从外面看,其他对象不知道究竟哪个接收者进行了哪些动作,只知道如果调用execute()方法,请求的目的就能达到。
也看到利用命令来参数化对象的一些例子。餐厅中,一整天下来,女招待参数化许多订单。在简单遥控器中,先用一个“打开电灯”命令加载按钮插槽,稍后将命令替换为“打开车库门”命令。插槽不管是什么命令对象,只要该对象实现command接口即可。
使用命令模式实现“队列、日志和支持撤销操作”,这是基本命令模式的直接扩展。有了足够的基础,可轻易地持有Meta Command Pattern,可以创建命令的宏,以便一次执行多个命令。
完整版
文档:
设计的遥控器API,主要设计目标是让遥控器代码尽可能简单,这样,新的厂商类一旦出现,遥控器不需要随之修改,采用命令模式,从逻辑上将遥控器和厂商的类解耦。这样,降低遥控器的生产成本,减少维护费用。
下面的类图提供了设计的全貌:
party模式的遥控器
宏命令
public class MacroCommand implements Command {
Command[] commands;
public MacroCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
for (int i = 0; i < commands.length; i++) {
commands[i].execute();
}
}
@Override
public void undo() {
for(int i= commands.length-1;i>=0;i--){
commands[i].undo();
}
}
}
使用
public class RemoteLoader {
public static void main(String[] args) {
RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
Light light = new Light("Living room");
TV tv = new TV("Living room");
Stereo stereo = new Stereo("Living room");
Hottub hottub = new Hottub();
LightOnCommand lightOn = new LightOnCommand(light);
StereoOnCommand stereoOn = new StereoOnCommand(stereo);
TVOnCommand tvOn = new TVOnCommand(tv);
HottubOnCommand hottubOn = new HottubOnCommand(hottub);
LightOffCommand lightOff = new LightOffCommand(light);
StereoOffCommand stereoOff = new StereoOffCommand(stereo);
TVOffCommand tvOff = new TVOffCommand(tv);
HottubOffCommand hottubOff = new HottubOffCommand(hottub);
// 两个数组,一个开启命令,一个关闭命令
Command[] partyOn = { lightOn,stereoOn, tvOn, hottubOn };
Command[] partyOff = { lightOff,stereoOff,tvOff,hottubOff };
MacroCommand partyOnMacro = new MacroCommand(partyOn);
MacroCommand partyOffMacro = new MacroCommand(partyOff);
// 将宏命令指定给希望的按钮
remoteControl.setCommand(0,partyOnMacro,partyOffMacro);
System.out.println(remoteControl);
System.out.println("----- Pushing Macro on -----");
remoteControl.onButtonPressed(0);
System.out.println("----- Pushing Macro off -------");
remoteControl.offButtonPressed(0);
}
}
问答
如何实现多层次的撤销操作
不要只记录最后一个被执行的命令,而是使用堆栈记录操作过程的每个命令,然后,不管什么时候按下撤销按钮,都可以从堆栈中取出最上层的命令,然后调用undo方法
命令模式更多用途:队列请求
命令可以将运算块打包(一个接收者和一组动作),然后将它传来传去,就像是一般的对象一样。即使命令对象被创建很久后,运算依然可以被调用。甚至可以在不同的线程中被调用。利用该特性衍生一些应用,如日程安排Scheduler,线程池,工作队列等。
想象有一个工作队列:你在某一端添加命令,然后另一端是线程,线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令…
实现命令接口的对象如:CompilerTask RayTrace FinacialComputation NetworkFetch DownloadRequest DistributeComputation 等命令
请注意,工作队列和进行计算的对象之间是完全解耦的,此刻线程可能在做财务运算,下一刻却在读取网络数据。工作线程不在乎到底做什么,只知道取出命令对象,然后调用execute()方法。类似的,只要实现命令模式的对象,就可以放入队列,线程可用时,调用此对象的execute()方法。
命令模式的更多用途:日志请求
某些应用需要我们将所有的动作都记录在日志中,并能在系统死机后,重新调用这些动作恢复到之前的状态。通过新增两个方法store()和load(),命令模式可支持。在java中,可序列化实现这些方法,一般认为序列化最好只用在对象的持久化上。
怎么做?执行命令时,将历史记录储存到磁盘上。一旦死机,可以将命令对象重新加载,并成批的依次调用这些对象的execute()方法。
有许多调用大型数据结构的动作的应用无法在每次改变发生时被快速的存储。通过使用记录日志,可将上次检查点checkpoint之后的操作记录下来,如果系统出状况,从检查点开始应用这些操作。如对电子表格应用,可能想实现的错误恢复方式是将电子表格的操作记录在日志中,而不是每次电子表格一有变化就记录整个电子表格。对于更高级的应用,这些技巧可被扩展Wi应用到事务处理transaction中,也就是说,一整群操作,必须全部进行完成,或者没有进行任何的操作。