设计模式之命令模式
情景:
屋里有很多家用电器,你需要设计一个遥控器,来控制所有电器的使用。
如果在遥控器中添加电器类,那就使得遥控器和具体电器类过度耦合了,遥控器不应该知道电器的实现细节。
遥控器应该简单一些,我们都知道遥控器只要一些按钮,所能做的动作仅仅是按下按钮,所以不应该包含太多的控制逻辑。
所以,这里需要用命令模式,来将“动作的请求者”从“动作的执行者”对象中解耦。
设计一个命令对象,遥控器可以执行命令对象,而不关心命令具体是做什么。
在命令对象内部,有具体的电器类和命令的具体实现,以及公开给遥控器的执行方法。
一个简单的实现:
首先是一个命令接口,所有的命令都要实现这个接口,然后放到遥控器上,而遥控器只要用execute()执行就好了,不需要知道命令细节。
public interface Command { public void execute(); }
一个具体的命令,开灯命令。
public class LightOnCommand implements Command { Light light; public LightOnCommand(Light light) { this.light = light; } @Override public void execute() { light.on(); } }
灯:
public class Light { public void on() { System.out.println("灯亮了"); } public void off() { System.out.println("灯灭了"); } }
遥控器:遥控器上有插槽用来持有命令。(可以考虑成遥控器上一个按钮-_-#)
public class SimpleRemoteControl { Command slot; public SimpleRemoteControl() {} public void setCommand(Command command) { slot = command; } public void buttonWasPressed() { slot.execute(); } }
测试类:
public class RemoteControlTest { public static void main(String[] args) { SimpleRemoteControl remote = new SimpleRemoteControl(); Light light = new Light(); LightOnCommand lightOn = new LightOnCommand(light); remote.setCommand(lightOn); remote.buttonWasPressed(); } }
输出: 灯亮了
命令模式:将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销操作。
类图:
上面的遥控器只有一个命令,事实上有很多的电器需要控制,而且不仅需要开命令,也需要有关闭命令。
实现遥控器类:
有七个开启按钮和七个关闭按钮。
public class RemoteControl { Command[] onCommands; Command[] offCommands; 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; } } public void setCommand(int slot, Command onCommand, Command offCommand) { onCommands[slot] = onCommand; offCommands[slot] = offCommand; } public void onButtonWasPushed(int slot) { onCommands[slot].execute(); } public void offButtonWasPushed(int slot) { offCommands[slot].execute(); } public String toString() { StringBuffer stringBuff = new StringBuffer(); stringBuff.append("\n----- Remote Control -----\n"); for (int i = 0; i < onCommands.length; i++) { stringBuff.append("[slot" + i + "] " + onCommands[i].getClass().getName() + "\t\t" + offCommands[i].getClass().getName() + "\n"); } return stringBuff.toString(); } }
具体命令(太多了,不贴啦):
public class StereoOffWithCDCommand implements Command { Stereo stereo; public StereoOffWithCDCommand(Stereo stereo) { this.stereo = stereo; } public void execute() { stereo.off(); stereo.popCD(); } }
测试一下:
public class RemoteLoader { public static void main(String[] args) { RemoteControl remoteControl = new RemoteControl(); Light light = new Light(); GarageDoor garageDoor = new GarageDoor(); Stereo stereo = new Stereo(); LightOnCommand lightOnCommand = new LightOnCommand(light); LightOffCommand lightOffCommand = new LightOffCommand(light); GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor); GarageDoorCloseCommand garageDoorCloseCommand = new GarageDoorCloseCommand(garageDoor); StereoOnWithCDCommand stereoOnWithCDCommand = new StereoOnWithCDCommand(stereo); StereoOffWithCDCommand stereoOffWithCDCommand = new StereoOffWithCDCommand(stereo); remoteControl.setCommand(0, lightOnCommand, lightOffCommand); remoteControl.setCommand(1, garageDoorOpenCommand, garageDoorCloseCommand); remoteControl.setCommand(2, stereoOnWithCDCommand, stereoOffWithCDCommand); System.out.println(remoteControl); remoteControl.onButtonWasPushed(0); remoteControl.offButtonWasPushed(0); remoteControl.onButtonWasPushed(1); remoteControl.offButtonWasPushed(1); remoteControl.onButtonWasPushed(2); remoteControl.offButtonWasPushed(2); } }
输出:
----- Remote Control -----
[slot0] com.wenr.chapter6.LightOnCommand com.wenr.chapter6.LightOffCommand
[slot1] com.wenr.chapter6.GarageDoorOpenCommand com.wenr.chapter6.GarageDoorCloseCommand
[slot2] com.wenr.chapter6.StereoOnWithCDCommand com.wenr.chapter6.StereoOffWithCDCommand
[slot3] com.wenr.chapter6.NoCommand com.wenr.chapter6.NoCommand
[slot4] com.wenr.chapter6.NoCommand com.wenr.chapter6.NoCommand
[slot5] com.wenr.chapter6.NoCommand com.wenr.chapter6.NoCommand
[slot6] com.wenr.chapter6.NoCommand com.wenr.chapter6.NoCommand
灯亮了
灯灭了
车库门开了
车库门关了
音响已打开
在音响中放入CD
音响声音调到11
音响已关闭
在音响中取出CD
重点来了,敲黑板!
空对象模式:
有些按钮还有没有被分配命令,如果我们将其赋值为null的话,onButtonWasPressed()就要这样写:
public void onButtonWasPushed(int slot) { if (onCommands[slot] != null) onCommands[slot].execute(); }
为了减少判断的麻烦,可以为其付一个空命令,它是一个不做任何事情的对象,是一个空对象(null object)。
当你不想返回一个有意义的对象时,空对象就很有用。客户也可以将处理null的责任转移给空对象。
到目前为止,我们还有一个要求没有实现,那就是撤销功能,撤销上一条命令。
首先,修改Command接口:
public interface Command { public void execute(); public void undo(); }
然后修改遥控器类,很简单,只需要记录一下上一条命令就可以了。
public class RemoteControlWithUndo { Command[] onCommands; Command[] offCommands; Command undoCommand; // 记录前一个命令 public RemoteControlWithUndo() { 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; } public void setCommand(int slot, Command onCommand, Command offCommand) { onCommands[slot] = onCommand; offCommands[slot] = offCommand; } public void onButtonWasPushed(int slot) { onCommands[slot].execute(); undoCommand = onCommands[slot]; } public void offButtonWasPushed(int slot) { offCommands[slot].execute(); undoCommand = offCommands[slot]; } public void undoButtonWasPushed() { undoCommand.undo(); } }
再次测试一下:
public class RemoteLoader { public static void main(String[] args) { RemoteControlWithUndo remoteControl = new RemoteControlWithUndo(); Light light = new Light(); GarageDoor garageDoor = new GarageDoor(); Stereo stereo = new Stereo(); LightOnCommand lightOnCommand = new LightOnCommand(light); LightOffCommand lightOffCommand = new LightOffCommand(light); GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor); GarageDoorCloseCommand garageDoorCloseCommand = new GarageDoorCloseCommand(garageDoor); StereoOnWithCDCommand stereoOnWithCDCommand = new StereoOnWithCDCommand(stereo); StereoOffWithCDCommand stereoOffWithCDCommand = new StereoOffWithCDCommand(stereo); remoteControl.setCommand(0, lightOnCommand, lightOffCommand); remoteControl.setCommand(1, garageDoorOpenCommand, garageDoorCloseCommand); remoteControl.setCommand(2, stereoOnWithCDCommand, stereoOffWithCDCommand); remoteControl.onButtonWasPushed(0); remoteControl.offButtonWasPushed(0); remoteControl.undoButtonWasPushed(); remoteControl.onButtonWasPushed(1); remoteControl.offButtonWasPushed(1); remoteControl.undoButtonWasPushed(); } }
输出:
灯亮了
灯灭了
灯亮了
车库门开了
车库门关了
车库门开了
很好,基本功能都实现了。
现在的遥控器只能撤销前一条命令,如果想要连续撤销,可以用一个栈来保存运行过的命令。
新的需求来了,现在希望按下一个按钮可以做许多事情,嗯……比如你从外面回家了,你想开灯,打开电视,打开音响……你想睡觉了,你要关灯关电视关音响。有没有办法,用一个按钮做一系列事情?
定义一个宏命令:
public class MacroCommand implements Command { Command[] commands; public MacroCommand(Command[] commands) { this.commands = commands; } public void execute() { for (int i = 0; i < commands.length; i++) { commands[i].execute(); } } public void undo() { for (int i = 0; i < commands.length; i++) { commands[i].undo(); } } }
测试:
public class RemoteLoader3 { public static void main(String[] args) { RemoteControlWithUndo remoteControl = new RemoteControlWithUndo(); Light light = new Light(); GarageDoor garageDoor = new GarageDoor(); Stereo stereo = new Stereo(); LightOnCommand lightOnCommand = new LightOnCommand(light); LightOffCommand lightOffCommand = new LightOffCommand(light); StereoOnWithCDCommand stereoOnWithCDCommand = new StereoOnWithCDCommand(stereo); StereoOffWithCDCommand stereoOffWithCDCommand = new StereoOffWithCDCommand(stereo); Command[] onCommands = {lightOnCommand, stereoOnWithCDCommand}; Command[] offCommands = {lightOffCommand, stereoOffWithCDCommand}; MacroCommand onMacroCommand = new MacroCommand(onCommands); MacroCommand offMacroCommand = new MacroCommand(offCommands); remoteControl.setCommand(0, onMacroCommand, offMacroCommand); remoteControl.onButtonWasPushed(0); remoteControl.offButtonWasPushed(0); remoteControl.undoButtonWasPushed(); } }
命令可以将运算块打包(一个接收者和一组动作),然后将它传来传去,就像是一般的对象一样。
现在,即使在命令对象被创建许久之后,运算依然可以被调用。
事实上,它甚至可以在不同的线程中被调用。
我们可以利用这样的特性衍生一些应用
例如:“日程安排” “线程池” “工作队列” 等
工作队列和进行计算的对象之间完全是解耦的。
通过新增两个方法store()和load(),我们可以将所有的动作都记录在日志中,,并能在系统死机后,重新调用这些动作恢复到之前的状态。
interface Command { execute(); undo(); store(); load(); }