定义
命令模式(command pattern)将请求封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
从上面的类图可以看出,一个命令对象通过在特定接收者上绑定一组动作来封装一个请求。要达到这一点,命令对象将动作和接收者包装进对象中,这个对象只暴露出execute()/undo()方法,当execute()/undo()方法被调用的时候,接收者就会执行这些动作。站在客户的角度来看,它只要把接收者封装到命令对象,通过把命令对象设置到调用者,后续就由调用者去触发命令执行。把不同的命令对象作为参数传递给调用者,就会触发执行不同的命令。
代码实现
事实上每次接触到一个新的设计模式时,看到定义中的类图总觉得非常抽象,所以每次我都是先看具体的例子的代码实现,反过来再看定义中的类图的。例子的代码更加具体,直接,定义中的类图非常的抽象,通用,所以例子这部分是我组织文章的重点。
废话不多说了,本例要解决的问题是,遥控器的问题,遥控器有7个插槽,每个插槽有开/关两个按钮,每个插槽接上不同的家电就可以控制不同的家电,还有一个撤销按钮用来撤销上次的动作。由于不同的家电,给的控制API是不同的,该如何设计?
上面是类图,首先看Command接口的定义,它定义了2个方法,
public interface Command {
public void execute();
public void undo();
}
再看接收者(这里是不同的家电)给的API,
// 灯
public class Light {
String location; //灯的位置
int level; // 灯的亮度级别
public Light(String location) {
this.location = location;
}
public void on() {
level = 100;
System.out.println("Light is on");
}
public void off() {
level = 0;
System.out.println("Light is off");
}
public void dim(int level) {
this.level = level;
if (level == 0) {
off();
}
else {
System.out.println("Light is dimmed to " + level + "%");
}
}
public int getLevel() {
return level;
}
}
// 天花板的风扇
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(location + " ceiling fan is on high");
}
public void medium() {
speed = MEDIUM;
System.out.println(location + " ceiling fan is on medium");
}
public void low() {
speed = LOW;
System.out.println(location + " ceiling fan is on low");
}
public void off() {
speed = OFF;
System.out.println(location + " ceiling fan is off");
}
public int getSpeed() {
return speed;
}
}
下面是具体的命令对象,它封装了家电及控制家电的动作。
// 关灯命令
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.off();
}
public void undo() {
light.on();
}
}
// 开灯命令
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
public void undo() {
light.off();
}
}
// 开高档风扇命令
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();
}
}
}
// 开中档风扇命令
public class CeilingFanMediumCommand implements Command {
CeilingFan ceilingFan;
int prevSpeed;
public CeilingFanMediumCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.medium();
}
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 CeilingFanLowCommand implements Command {
CeilingFan ceilingFan;
int prevSpeed;
public CeilingFanLowCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.low();
}
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;
}
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.off();
}
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();
}
}
}
NoCommand对象是个null object
,用来初始化调用者,这样可保证调用者已经持有了命令对象,要么是NoCommand,什么都不做,要么是具体的命令对象,可以避免去做是否为空的判断。
public class NoCommand implements Command {
public void execute() { }
public void undo() { }
}
调用者对象,
import java.util.*;
public class RemoteControlWithUndo {
Command[] onCommands;
Command[] offCommands;
Command undoCommand;
public RemoteControlWithUndo() {
// 对应7个开关按钮
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;
}
// 设置第slot个插槽的开/关命令对象
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void onButtonWasPushed(int slot) {
// 这里可以避免去做onCommands[slot]是否为空的判断
onCommands[slot].execute();
undoCommand = onCommands[slot];
}
public void offButtonWasPushed(int slot) {
// 这里可以避免去做offCommands[slot]是否为空的判断
offCommands[slot].execute();
undoCommand = offCommands[slot];
}
public void undoButtonWasPushed() {
// 这里可以避免去做undoCommand是否为空的判断
undoCommand.undo();
}
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");
}
stringBuff.append("[undo] " + undoCommand.getClass().getName() + "\n");
return stringBuff.toString();
}
}
客户代码部分,也是测试启动代码的实现。
public class RemoteLoader {
public static void main(String[] args) {
// 调用者对象
RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
// 接收者对象,起居室的灯
Light livingRoomLight = new Light("Living Room");
// 命令对象
LightOnCommand livingRoomLightOn =
new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff =
new LightOffCommand(livingRoomLight);
// 安装插槽0 命令对象
remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
// 触发插槽0对应的命令对象执行
remoteControl.onButtonWasPushed(0); // 开起居室的灯
remoteControl.offButtonWasPushed(0); // 关起居室的灯
System.out.println(remoteControl);
remoteControl.undoButtonWasPushed(); // 执行撤销命令
remoteControl.offButtonWasPushed(0);
remoteControl.onButtonWasPushed(0);
System.out.println(remoteControl);
remoteControl.undoButtonWasPushed();
// 接收者对象,起居室中天花板吊扇
CeilingFan ceilingFan = new CeilingFan("Living Room");
CeilingFanMediumCommand ceilingFanMedium =
new CeilingFanMediumCommand(ceilingFan);
CeilingFanHighCommand ceilingFanHigh =
new CeilingFanHighCommand(ceilingFan);
CeilingFanOffCommand ceilingFanOff =
new CeilingFanOffCommand(ceilingFan);
remoteControl.setCommand(0, ceilingFanMedium, ceilingFanOff);
remoteControl.setCommand(1, ceilingFanHigh, ceilingFanOff);
remoteControl.onButtonWasPushed(0);
remoteControl.offButtonWasPushed(0);
System.out.println(remoteControl);
remoteControl.undoButtonWasPushed();
remoteControl.onButtonWasPushed(1);
System.out.println(remoteControl);
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(); } } }
这样客户就可以自定义命令组了,而且这个宏命令包含的命令组还可以运行时动态调整的,非常灵活。
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); remoteControl.onButtonWasPushed(0); remoteControl.offButtonWasPushed(0);
实现队列请求
因为命令对象封装了一个接收者和一个(或者组)动作,把命令对象作为参数传递,可以衍生出很多应用,如日程安排(scheduler),线程池(threadpool),工作队列(workqueue)等。
想象有一个工作队列,你在某一端添加命令,然后在另外一端则是线程。线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,执行完这个命令后丢弃,循环执行前面都动作。
实现日志请求
某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的动作。通过新增两个方法(store和load),命令模式就能支持这一点,在系统正常执行命令时,将每个命令都store在磁盘中,一旦系统死机重启后,将之前store的命令load出来,重新调用execute()即可。
public interface Command { public void execute(); public void undo(); public void store(Command[] cmd); public void load(); }
该模式体现了哪些OO原则
-
RemoteControlWithUndo的onButtonWasPushed/offButtonWasPushed方法的实现就符合该原则。
本章总结
命令模式将发出请求的对象和执行请求的对象解耦。
在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个(或者组)动作。
实际操作时,常使用“聪明”命令对象,也就是命令对象直接实现了请求,而不是将工作委托给接收者。
命令也可以用来实现日志和事务系统。