命令模式(the command pattern)
实现遥控器:
package remote; public class RemoteControl { Command[] onCommands; Command[] offCommands; public RemoteControl() { onCommands=new Command[7]; offCommands=new Command[7]; NoCommand 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()+" "+offCommands[i].getClass().getName()+"\n"); } return stringBuff.toString(); } }
覆盖toString(),打印出每个插槽和他对应的命令。
实现命令。
我们前面已经写了LightOnCommand,关闭命令没什么不同。看起来是这样:
public class LightOffCommand implements Command { Light light; public LightOffCommand(Light light) { this.light = light; } public void execute() { light.off(); } }
让我们来提高挑战性,如何为音响(Stereo)编写开与关的命令?好了,很容易,虽然开音响有很多步骤,可以全写在execute(),就可以打开音响了。
public class Stereo { String location; public Stereo(String location) { this.location = location; } public void on() { System.out.println(location + " stereo is on"); } public void off() { System.out.println(location + " stereo is off"); } public void setCD() { System.out.println(location + " stereo is set for CD input"); } public void setDVD() { System.out.println(location + " stereo is set for DVD input"); } public void setRadio() { System.out.println(location + " stereo is set for Radio"); } public void setVolume(int volume) { // code to set the volume // valid range: 1-11 (after all 11 is better than 10, right?) System.out.println(location + " Stereo volume set to " + volume); } }
public class StereoOnWithCDCommand implements Command { Stereo stereo; public StereoOnWithCDCommand(Stereo stereo) { this.stereo = stereo; } public void execute() { stereo.on(); stereo.setCD(); stereo.setVolume(11); } }
其余的厂商类都类似这样。
逐步测试遥控器
我们剩下了要做的就是运行测试和准备api的说明文档了。
public class RemoteLoader { public static void main(String[] args) { RemoteControl remoteControl=new RemoteControl(); Light livingRoomLight=new Light("living Room"); Light kitchenLight=new Light("Kitchen"); Stereo stereo=new Stereo("living room"); LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight); LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight); LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight); LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight); StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo); StereoOffCommand stereoOff = new StereoOffCommand(stereo); remoteControl.setCommand(0,livingRoomLightOn,livingRoomLightOff); remoteControl.setCommand(1,kitchenLightOn,kitchenLightOff); remoteControl.setCommand(2,stereoOnWithCD,stereoOff); System.out.println(remoteControl); remoteControl.onButtonWasPushed(0); remoteControl.offButtonWasPushed(0); remoteControl.onButtonWasPushed(2); remoteControl.offButtonWasPushed(2); } }
输出:
----------Remote Control-----------
[slot0]remote.LightOnCommand remote.LightOffCommand
[slot1]remote.LightOnCommand remote.LightOffCommand
[slot2]remote.StereoOnWithCDCommand remote.StereoOffCommand
[slot3]remote.NoCommand remote.NoCommand
[slot4]remote.NoCommand remote.NoCommand
[slot5]remote.NoCommand remote.NoCommand
[slot6]remote.NoCommand remote.NoCommand
living Room light is on
living Room light is off
注意:
System.out.println(remoteControl);
会调用对象的toString方法,
等一下,NoCommand是什么?在遥控器中,我们不想每次都检查是否某个插槽都加载了命令,比方说,在onButtonWasPushed()方法中,我们可能需要这样的代码:
public void onButtonWasPushed(int slot)
{
if(onCommands[slot]!=null){
onCommands[slot].execute();
}
}
所以,如何避免上面的代码呢?实现一个不做事情的命令。
public class NoCommand implements Command{
public void execute() { }
}
这么一来,在构造器中,我们将每个插槽都预先指定成NoCommand对象,以便确定每个插槽永远都有命令对象。
Nocommand对象是一个空对象(null object)的例子。当你不想返回一个有意义的对象时,空对象就很有用。客户也可以将处理null的责任转移给空对象。
写文档的时刻终于到了。。。
我们还忘了实现undo 命令!
1.当命令支持undo时,该命令就必须提供和execute()方法相反的undo()方法,不过execute()刚才做了什么,undo()都会倒转过来。这么一来,在各个命令中加入undo之前,我们必须现在Command接口中加入undo()方法。
public interface Command {
public void execute();
public void undo();
}
让我们深入电灯的命令,实现undo()方法。
2.我们先从LightOnCommand开始,如果他的execute()被调用,那么最后被调用的是on()方法,我们知道undo()需要调用off()方法进行相反的动作。
public class LightOnCommand implements Command{ Light light; public LightOnCommand(Light light) { this.light=light; } public void execute() { light.on(); } public void undo() { light.off(); } }
现在处理LightOffCommand,这里,undo()需要调用on()方法:
public class LightOffCommand implements Command { Light light; public LightOffCommand(Light light) { this.light=light; } public void execute() { light.off(); } public void undo() { light.on(); } }
事情还没完,我们还要花些力气,让遥控器能够追踪最后被按下的什么按钮。
3.要加上对undo按钮的支持,我们需要加入一个新的实例变量,用来追踪最后的命令。然后,不管何时撤销按钮被按下,我们都可以取出这个命令并调用它的undo()方法。
package remote; public class RemoteControl { Command[] onCommands; Command[] offCommands; Command undoCommand; public RemoteControl() { onCommands=new Command[7]; offCommands=new Command[7]; NoCommand 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 String toString() { .... } }
在测试程序中,我们调用remoteControl.undoButtonWasPushed()就可以undo了。
remoteControl.onButtonWasPushed(0);
remoteControl.offButtonWasPushed(0);
remoteControl.onButtonWasPushed(2);
remoteControl.undoButtonWasPushed();
remoteControl.offButtonWasPushed(2);
使用状态实现撤销
好了,实现电灯的撤销是有意义的,但也实在太简单了。通常,想要实现undo的功能,需要记录一些状态。让我们试一个有趣的例子,厂商中的吊扇允许有多种转速,当然也允许被关闭。
吊扇的源代码如下:
public class CeilingFan { String location = ""; int level; public static final int HIGH = 2; public static final int MEDIUM = 1; public static final int LOW = 0; public CeilingFan(String location) { this.location = location; } public void high() { // turns the ceiling fan on to high level = HIGH; System.out.println(location + " ceiling fan is on high"); } public void medium() { // turns the ceiling fan on to medium level = MEDIUM; System.out.println(location + " ceiling fan is on medium"); } public void low() { // turns the ceiling fan on to low level = LOW; System.out.println(location + " ceiling fan is on low"); } public void off() { // turns the ceiling fan off level = 0; System.out.println(location + " ceiling fan is off"); } public int getSpeed() { return level; } }
加入撤销到吊扇的命令类
我们想要实现吊扇的undo,需要追踪吊扇的最后设置速度。如果undo()被调用了,就要恢复为之前的速度。下面是
public class CeilingFanHighCommand implements Command { CeilingFan ceilingFan; int prevSpeed; public CeilingFanHighCommand(CeilingFan ceilingFan) { this.ceilingFan = ceilingFan; } public void execute() { prevSpeed = ceilingFan.getSpeed(); ceilingFan.high(); } public void undo() { if (prevSpeed == CeilingFan.HIGH) { ceilingFan.high(); } else if (prevSpeed == CeilingFan.MEDIUM) { ceilingFan.medium(); } else if (prevSpeed == CeilingFan.LOW) { ceilingFan.low(); } else if (prevSpeed == CeilingFan.OFF) { ceilingFan.off(); } } }
low,medium.off命令类似上面的。
测试代码简写如下:
CeilingFan ceilingFan = new CeilingFan("Living Room"); CeilingFanHighCommand ceilingFanHigh = new CeilingFanHighCommand(ceilingFan); CeilingFanLowCommand ceilingFanLow = new CeilingFanLowCommand(ceilingFan); CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan); remoteControl.setCommand(3, ceilingFanHigh, ceilingFanOff); remoteControl.setCommand(4, ceilingFanLow, ceilingFanOff); remoteControl.onButtonWasPushed(3); remoteControl.onButtonWasPushed(4); remoteControl.undoButtonWasPushed();
每个遥控器都需具备“party模式”!
如果拥有了一个遥控器,却无法光凭按下一个按钮,就同时能弄按灯光,打开音响和电视,设置好dVd,并让热水器开始加温,那么要这个遥控器还有什么意义?
一般的想法是,制造一种新的命令,用来执行其他一堆命令,而不只是执行一个命令!
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(); } } /** * NOTE: these commands have to be done backwards to ensure proper undo functionality */ public void undo() { for (int i = commands.length -1; i >= 0; i--) { commands[i].undo(); } } }
使用宏命令
1.先创建想要进入宏的命令集合:
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);
2.接下来创建2个数组,其中一个用来记录开启命令,另一个用来关闭命令,并在数组内放入相应的命令。
Command[] partyOn = { lightOn, stereoOn, tvOn, hottubOn}; Command[] partyOff = { lightOff, stereoOff, tvOff, hottubOff}; MacroCommand partyOnMacro = new MacroCommand(partyOn); MacroCommand partyOffMacro = new MacroCommand(partyOff);
3.然后将宏命令指定给我们希望的按钮
remoteControl.setCommand(0, partyOnMacro, partyOffMacro);
问题:
我如何能够实现多层次的undo操作?
其实很容易,不要只是记录最后一个执行的命令,而使用一个堆栈记录来操作过程的每一个命令。然后,不管什么时候按下了undo按钮,都可以从堆栈中取出最上层的命令,然后调用undo方法。
命令模式的更多用途:队列请求和日志请求
命令可以将运算快打包(一个接收者和一组动作)。然后将它传来传去,就像是一般的对象一样。现在,即使在命令对象被创建许久之后,运算依然可以被调用。事实上,他甚至可以在不同的线程中被调用。我们可以利用这样的特性衍生一些应用,例如:日程安排(scheduler,线程池,工作队列等.
总结:在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作。
调用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用。
命令可以支持撤销,做法是实现一个undo()方法来回到execute()被执行前的状态。
命令可以用来实现日志和事务系统。