Head First 设计模式总结(五) 命令模式
本文总结了《Head First 设计模式》中的命令模式
命令模式——将请求封装成“对象”,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持撤销操作。
问题描述:
某电气自动化公司提供了一个可编程遥控器,遥控器上有若干个可编程插槽(Slot),用户可以为每个插槽添加不同的代码,让它们得以控制不同的家用电器。该公司还提供了很多电器类(电灯、电视机、音箱等),因为遥控器只能指挥电器执行相关操作,至于电器们“如何”执行动作,遥控器是不需要管的。电灯的“开、关”、音箱的“音量调大、调小”等操作都是电器们自身完成的。
现在遥控器已经到手了,家用电器的用法代码也有了,需要做的就是给遥控器编程,让它去指挥家用电器们。
书中用餐馆里面顾客订餐的过程引入
这个引入强调了对象之间的“解耦”问题,女招待员只需要将订单从顾客手里拿到并递交给厨师就行了,她只知道“有”、“无”订单,并不知道顾客点了哪些东西,也不知道厨师是怎么制作订单上的菜品的。订单将女招待员和厨师解耦了,他们俩不需要去沟通细节上的东西。
餐厅问题正反映了命令模式,下面给出命令模式的结构图:
上图的Command=“订单”,Invoker=“女招待员”,Receiver=“厨师“,Client=”顾客”。Invoker和Receiver解耦了,Invoker需要做的就是通过setCommand()方法,让之前从Client手中接下的command去命令Receiver执行相关操作。
下面将命令模式应用到之前的可编程遥控器问题上。
在遥控器问题中,Command=“开灯”、“关灯”、“调大音量”等,Invoker=“遥控器”,Receiver=“各种家用电器”,Client=“房屋主人”,这里遥控器和家用电器之间解耦了。房屋主人通过setCommand()方法创建了很多Command对象在遥控器上的插槽(Slot)上,由于Command很多,可以将Command放在数组里,数组以Slot为编号,这样Command[slot]就代表一个Command。注意这里setCommand()方法是遥控器的方法,是房屋主人创建了Command对象和遥控器对象之后,通过“遥控器.setCommand()”给不同的Slot赋予不同的Command。
将所有Command(类似于按钮)部署在遥控器上之后,还需要赋予每个Command去操控家用电器的能力。因此,每个Command都得有一个自己的execute()方法,这个方法能控制家用电器,例如,LightOnCommand的execute()方法就是让一盏灯打开。当然,一盏灯怎么打开只有灯它自己最清楚,因此LightOnCommand得利用一个具体的Light(具体家用电器)对象去执行动作,可以通过LightOnCommand的构造器将Light对象传进来,如果要让客厅的灯打开,就把客厅的Light对象传进来,然后在execute()中调用Light对象的On()方法,就行了。这里的Light就是上图中的一个Receiver。
给出书上关于该问题的图
至此,命令模式的思路已经大致清晰了,下面给出书中的代码。
第一步:先弄一个Command接口
public interface Command {
public void execute();
}
第二步:弄一个具体的Command类
public class LightOnCommand implements Command {
Light light;
LightOnCommand(Light light){
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
电气自动化公司提供的家用电器Light类的代码应该是类似如下的形式:
public class Light {
String type;
Light(String type){
this.type = type;
}
public void on(){
System.out.println(type + " Light is on!");
}
public void off(){
System.out.println(type + " Light is off !");
}
}
第三步:完成遥控器类的编写
public class RemoteControl {
Command[] onCommands;
Command[] offCommands;
public RemoteControl(){
//创建数组用于装Command
onCommands = new Command[7];
offCommands = new Command[7];
Command noCommand = new NoCommand(); //用到了空对象初始化
for (int i=0; i<7;i++){
onCommands[i] = noCommand;//没有被明确指定命令的插槽,会被赋予NoCommand对象
offCommands[i] = noCommand;
}
}
//给指定Slot部署相应的Commands
public void setCommands(int slot,Command onCommand,Command offCommand){
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
//按键事件一旦发生就执行execute()
public void onButtonWasPushed(int slot){
onCommands[slot].execute();
}
public void offButtonWasPushed(int slot){
offCommands[slot].execute();
}
}
这里用到了空对象(NoCommand),这本身也是一种设计模式,这个可编程遥控器刚到用户手中的时候是没有控制功能的,按下按钮可能没有任何用,这时候就可以把“ 任何事都不做 ” 的NoCommand对象赋予这个按钮,用于按钮的初始化。
空对象的代码如下:
public class NoCommand implements Command {
@Override
public void execute() {} //什么事都不做
}
开灯命令的代码如下:
public class LightOnCommand implements Command {
Light light;
LightOnCommand(Light light){
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
关灯命令的代码如下:
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light){
this.light = light;
}
@Override
public void execute() {
light.off();
}
}
第四步:将要用的Command设置到遥控器上,并测试
public class RemoteLoader {
public static void main(String[] args){
//创建遥控器对象
RemoteControl remoteControl = new RemoteControl();
//创建家用电器对象
Light livingRoomLight = new Light("Living room");
Light kitchenLight = new Light("Kitchen");
//创建一些命令灯的Command
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);
//让0号插槽去控制客厅灯的开关
remoteControl.setCommands(0,livingRoomLightOn,livingRoomLightOff);
//让1号插槽去控制厨房灯的开关
remoteControl.setCommands(1,kitchenLightOn,kitchenLightOff);
//房屋主人按下按钮去控制灯的开关
//按下0号插槽的开按钮
remoteControl.onButtonWasPushed(0);
//按下1号插槽的关按钮
remoteControl.offButtonWasPushed(1);
//按下2号插槽的开按钮
remoteControl.onButtonWasPushed(2);
}
}
测试结果如下:
Living room Light is on!
Kitchen Light is off!
可以看到按下2号插槽的开按钮没有任何输出,因为我们没有给2号插槽设定任何Command,它根本不知道去打开谁,事实上它被空指令对象NoCommand初始化了,而NoCommand对象就是什么都不做。
下面是整个设计的全貌图:
撤销
还可以为遥控器增加撤销功能,书中的实现如下:
新建了一个撤销按钮,一旦撤销按钮被按下,将会执行遥控器先前执行的一个命令
宏命令
为了让遥控器的功能更高级,让它的一个按钮能一次性操作多个家用电器,书中还介绍了宏命令。这是通过宏指令来实现的,这是一个能同时调用多个命令的命令,代码如下:
public class MacroCommand implements Command {
Command[] commands;
public MacroCommand(Command[] commands){
this.commands = commands;
}
@Override
public void execute() {
for (int i = 0;i<commands.length;i++){
commands[i].execute();
}
}
}
设置一些将要加入宏命令的电器和正常指令
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);
LightOffCommand lightOff = new LightOffCommand(light);
StereoOnCommand stereoOn = new SteroOncommand(stereo);
StereoOnCommand stereoOff = new SteroOffcommand(stereo);
TVOnCommand tvOn = new TVOnCommand(tv);
TVOffCommand tvOff = new TVOffCommand(tv);
HottubOnCommand hottubOn = new HottubOnCommand(hottub);
HottubOffCommand hottubOff = new HottubOffCommand(hottub);
将这些命令装进命令数组中
Command[] partyOn = {lightOn,stereoOn,tvOn,hottubOn};
Command[] partyOff = {lightOff,stereoOff,tvOff,hottubOff};
将命令数组封装成宏命令
MacroCommand partyOnMacro = new MacroCommand(partyOn);
MacroCommand partyOffMacro = new MacroCommand(partyOff);
将宏命令和0插槽绑定起来
remoteControl.setCommand(0,partyOnMacro,partyOffMacro);
下面一行代码将使之前的所有电器打开
remoteControl.onButtonWasPushed(0);
书中还介绍了命令模式的其他用途:
队列请求
直接上图