1.这一节我们的任务是创建一个类似智能家居的万能遥控器,控制各种家电。我们需要将“请求”封装成对象(一个命令对象通过在特定接收者上绑定一组动作来封装请求),以便使用不同的请求、队列、或者日志来参数化其对象——这就是命令模式。
2.我们具体来看一个例子:
首先我们要完成对命令的对象封装:
public interface Command {
public 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");
}
}
我们可以通过上述接口封装“开灯”这个命令,这个就是所谓的命令对象,它把动作和接收者包进对象之中,只暴露一个execute方法:
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
而我们的遥控器对于上述封装要一无所知,这样才能做到解耦:
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() {
}
}
你可能会注意到一个叫做noCommand的对象出现在初始化遥控器对象时。这个是个小技巧,我们并不想每次都检查某个插槽是不是都绑定了命令,那么我们就引入了这个东东,实现一个不做事情的命令:
public class NoCommand implements Command {
public void execute() { }
}
这样初始化就让每一个插槽都有命令了,以后若不进一步指明命令的插槽,那么就是默认这个noCommand对象。这就是一个典型的“空对象”例子,当你不想返回一个有意义的对象时,空对象就十分有用,此时空对象可以做成判断NULL或者提示“未绑定”信息的工作。
3.我们在命令模式中也可以设置类似undo的撤销命令来撤销发出的命令请求:
public interface Command {
public void execute();
public void undo();
}
我们还是拿开电灯这个操作来说明,毕竟这足够简单:对于一个开灯命令,其对应的撤销命令自然是“关电灯”:
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
public void undo() {
light.off();
}
}
虽然这简单之极,这还没完,我们必须让遥控器记住上次到底做了什么操作,才可能去撤销:
package headfirst.command.undo;
import java.util.*;
//
// This is the invoker
//
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 String toString() {
}
}
对于电灯,我们只有两种状态,但是要是多个状态呢?我们举一个吊扇的例子,吊扇有多个转速,高中低关,四个状态:
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操作。
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() {
switch (prevSpeed) {
case CeilingFan.HIGH: ceilingFan.high(); break;
case CeilingFan.MEDIUM: ceilingFan.medium(); break;
case CeilingFan.LOW: ceilingFan.low(); break;
default: ceilingFan.off(); break;
}
}
}
当然你也可以实现诸如多个命令批量执行和完成多个撤销操作。在这个例子中,还可以将电灯和电风扇等抽象为电器这个抽象类,然后启开关等两种电器的通用操作就可以写一个命令类完成,而不需要单独写“开电灯”“开电扇”这样的针对特定电器的命令类了。
命令模式可以用于工作队列和日志操作等方面。