命令模式——把方法调用封装起来,我们可以把运算块包装成形。所以调用此运算的对象不需要关心事情是如何进行的,只要知道我们包装完的方法即可。
案例
制作一个家电自动化遥控器的 API。这个遥控器具有七个可编程的插槽(每个都可以指定到一个不同的家电装置),每个插槽都有对应的开关按钮。这个遥控器还具备一个整体的撤销按钮。
首先我们需要让所有的命令对象实现相同的包含一个方法的接口。
public interface Command {
public void execute();
}
然后创建一个打开电灯的命令,这里面包含一个灯的对象,还有实现命令的方法。我们调用 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("Light on");
}
}
现在灯的对象,以及命令对象也都创建完成了,我们还需要什么?想都不用想,遥控器对象呀!
public class SimpleRemoteControl {
Command slot;
public SimpleRemoteControl() {}
public void setCommand(Command command) {
this.slot = command;
}
public void buttonWasPressed() {
slot.execute();
}
}
这是一个简单的遥控器对象,内含一个命令属性,一个设置命令的 setter 方法,还有一个表示遥控器按钮按下的方法,buttonWasPressed()。当我们想要按按钮时,只需要调用这个方法即可。
现在我们来简单测试一下。
public class RemoteControlTest {
public static void main(String[] args) {
SimpleRemoteControl control = new SimpleRemoteControl();
Light light = new Light();
LightOnCommand lightOnCommand = new LightOnCommand(light);
control.setCommand(lightOnCommand);
control.buttonWasPressed();
}
}
首先我们创建了一个遥控器对象,然后创建了一个灯的对象和打开灯的命令对象,把这个命令对象设置到遥控器对象中,调用按动按钮的方法。会看到控制台输出Light on
。
到这里我们已经实现了一部分遥控器的 API,事实上接下来无非也就是重复操作,增加新的需要操作的单元,比如门,增加它的类,然后再增加一个实现过 Command 接口的命令类,再通过 setCommand(Command command) 对象即可。
命令模式将「请求」封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
我们来仔细看这个定义,一个命令对象通过在特定的接收者上绑定一组动作然后封装成一个请求,所以我们这个命令对象就需要将接收者和还有动作封装到对象中,然后这个对象只暴露 execute() 方法,当此方法被调用时,这些动作就会进行,而调用者不知道是哪个接收者进行了什么动作,只知道我们要完成什么目的只需要调用 execute() 方法。
命令对象就相当于中介,一方提供房子,另一方只提供钱,这样他买房子的目的就达到了。中介实现 Command 接口,卖家把房子注册到中介中,然后经过打扫,装修等一系列步骤后,卖家只需要知道,我付钱给中介,调用 execute() 方法就可以得到房子,完成自己买房的目的。
但我们刚刚只是定义了只有一个按钮的简单遥控器,我们现在在它的基础上实现一个多按钮的遥控器。
public class RemoteControl {
ArrayList<Command> onCommands;
ArrayList<Command> offCommands;
public RemoteControl() {
this.onCommands = new ArrayList<>();
this.offCommands = new ArrayList<>();
}
public void setCommands(Command onCommand, Command offCommand) {
this.onCommands.add(onCommand);
this.offCommands.add(offCommand);
}
public void onButtonWasPushed(int slot) {
this.onCommands.get(slot).execute();
}
public void offButtonWasPushed(int slot) {
this.offCommands.get(slot).execute();
}
}
分别通过一个集合去接收对象开的命令和关的命令,然后同时设置两个命令以保证索引一致。再在对象类中加入 off() 方法,表明对象关闭或回收的意思。
public class Light {
public void on() {
System.out.println("Light on");
}
public void off() {
System.out.println("Light off");
}
}
再新创建一个表示关灯的命令。
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
}
最后来看测试类,先创建遥控器对象,然后创建要控制的元件对象,接着创建表示开关的命令对象,在遥控器对象中设置好命令对象,最后调用按钮执行。
public class RemoteControlTest {
public static void main(String[] args) {
RemoteControl remoteControl = new RemoteControl();
Light light = new Light();
GarageDoor garageDoor = new GarageDoor();
LightOnCommand lightOnCommand = new LightOnCommand(light);
LightOffCommand lightOffCommand = new LightOffCommand(light);
GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
GarageDoorCloseCommand garageDoorCloseCommand = new GarageDoorCloseCommand(garageDoor);
remoteControl.setCommands(lightOnCommand, lightOffCommand);
remoteControl.setCommands(garageDoorOpenCommand, garageDoorCloseCommand);
remoteControl.onButtonWasPushed(1);
remoteControl.offButtonWasPushed(1);
remoteControl.onButtonWasPushed(2);
remoteControl.offButtonWasPushed(2);
}
}
可以加入撤销 undo() 方法,这就是相当与一个取反操作,比如开灯的命令,execute() 方法是 light.on(),而 undo() 就是执行 light.off() 方法。
现在我们就加上 undo() 方法,修改后的 Command 接口。
public interface Command {
public void execute();
public void undo();
}
然后是修改后的命令实现类,LightOnCommand、LightOffCommand。
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off();
}
}
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
@Override
public void undo() {
light.on();
}
}
现在我们就到了修改遥控器的步骤,来看看遥控类 RemoteControlWithUndo 类。
public class RemoteControlWithUndo {
Command[] onCommands;
Command[] offCommands;
Command undoCommand;
public RemoteControlWithUndo() {
onCommands = new Command[7];
offCommands = new Command[7];
NoCommand noCommand = new NoCommand();
for (int i = 0; i < onCommands.length; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
undoCommand = noCommand;
}
public void setCommands(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();
}
}
我们新增了一个命令属性 undoCommand,然后初始化时指定一个空的命令,这次我使用命令数组代替命令集合,为了防止出现命令调用时出错,我们将整个数组每一个元素都初始化为空命令。在每次调用按钮按下的方法时,都设置 undoCommand 为刚刚调用的命令,这样我们在 undoButtonWasPushed() 方法中才可以通过 undoCommand.undo() 进行命令的撤销。
现在是测试时间。
public class RemoteControlWithUndoTest {
public static void main(String[] args) {
RemoteControlWithUndo remoteControlWithUndo = new RemoteControlWithUndo();
Light light = new Light();
LightOnCommand lightOnCommand = new LightOnCommand(light);
LightOffCommand lightOffCommand = new LightOffCommand(light);
remoteControlWithUndo.setCommands(0, lightOnCommand, lightOffCommand);
remoteControlWithUndo.onButtonWasPushed(0);
remoteControlWithUndo.offButtonWasPushed(0);
remoteControlWithUndo.undoButtonWasPushed();
remoteControlWithUndo.offButtonWasPushed(0);
remoteControlWithUndo.undoButtonWasPushed();
}
}
创建完遥控器、灯的对象后,创建开灯关灯的命令,设置命令,调用按钮 on、off 和 undo 方法即可测试我们的小遥控器是否达到了我们的目的。
现在我们还可以进行改进,使用状态实现撤销,现在我们通过状态值实现撤销,比如设置一个电风扇的转速,高速、中速、低速和关闭状态。现在我们来看看代码。
public class CeilingFan {
public static final int HIGH = 3;
public static final int MEDIUM = 2;
public static final int LOW = 1;
public static final int OFF = 0;
String location;
int speed;
public CeilingFan(String location) {
this.location = location;
speed = OFF;
}
/**
* 设置高转速
*/
public void high() {
speed = HIGH;
System.out.println("high");
}
/**
* 设置中转速
*/
public void medium() {
speed = MEDIUM;
System.out.println("medium");
}
/**
* 设置低转速
*/
public void low() {
speed = LOW;
System.out.println("low");
}
/**
* 关闭吊扇
*/
public void off() {
speed = OFF;
System.out.println("off");
}
public int getSpeed() {
return speed;
}
}
吊扇一共有四个状态,关闭、低速、中速和高速,所以有四个切换速度的方法,还有一个获取当前速度的方法,下面看看高速、中速和关闭命令类。
public class CeilingFanHighCommand implements Command {
CeilingFan ceilingFan;
int prevSpeed;
public CeilingFanHighCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.high();
}
@Override
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();
}
}
}
public class CeilingFanMediumCommand implements Command {
CeilingFan ceilingFan;
int prevSpeed;
public CeilingFanMediumCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.medium();
}
@Override
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();
}
}
}
public class CeilingFanOffCommand implements Command {
CeilingFan ceilingFan;
int prevSpeed;
public CeilingFanOffCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
@Override
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.off();
}
@Override
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();
}
}
}
有一个状态接收吊扇之前的状态,当状态接收完再改变吊扇的状态,还有一个 undo() 方法,通过判断 prevSpeed 的状态撤销该命令。
测试类,和之前步骤一样,测试无误即可。
public class RemoteTest {
public static void main(String[] args) {
RemoteControlWithUndo remoteControlWithUndo = new RemoteControlWithUndo();
CeilingFan ceilingFan = new CeilingFan("Room");
CeilingFanMediumCommand ceilingFanMediumCommand = new CeilingFanMediumCommand(ceilingFan);
CeilingFanHighCommand ceilingFanHighCommand = new CeilingFanHighCommand(ceilingFan);
CeilingFanOffCommand ceilingFanOffCommand = new CeilingFanOffCommand(ceilingFan);
remoteControlWithUndo.setCommands(0, ceilingFanHighCommand, ceilingFanOffCommand);
remoteControlWithUndo.setCommands(1, ceilingFanMediumCommand, ceilingFanOffCommand);
remoteControlWithUndo.onButtonWasPushed(0);
remoteControlWithUndo.offButtonWasPushed(0);
remoteControlWithUndo.undoButtonWasPushed();
remoteControlWithUndo.onButtonWasPushed(1);
remoteControlWithUndo.undoButtonWasPushed();
}
}
这样我们通过状态来实现撤销就完成了。接下来我们来看看宏命令,其实很简单,就是将execute() 方法修改,遍历我们的 onCommands 数组,挨个调用 execute() 方法即可。
整个命令模式已经到了尾声,就这么多吧,再复习一下定义。
命令模式将「请求」封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。