命令模式
1、定义
命令模式 将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
粗看这个定义有些拗口难懂,我们挑选几个点进行分析,首先命令模式是将请求封装成对象的,在之前的编码中我们要调用某个类的某个方法时是直接new一个对象然后调用这个对象的方法即可,使用了命令模式之后我们不再直接调用方法,而是通过将使用方法封装成对象,我们调用的是对象暴露出来的接口,这样做有什么好处呢?我们会在下面的实例中进行说明。另一点是命令模式支持撤销操作,也就是说,我们在调用了接口执行方法之后也可以将这个方法的操作进行撤销,这也是命令模式的一大特点。下面我们通过具体实例来讲解一下命令模式。
2、命令模式案例
2.1、需求
假设我们想为一个遥控器编辑一套代码,这个遥控器有七排按钮,每排分别有两个按钮,分别是开启按钮和关闭按钮,用于控制某个家用电器的启动和关闭,另外此遥控器还能够实现撤销上一步操作的功能。家用电器的开启和关闭方法由生产商提供,我们编辑的代码只需要调用这些方法即可。
2.2、定义通用电器类
在介绍具体实现方式之前,我们先定义几个家用电器类,在实例中这些家用电器如何实现电器的开关不需要我们关心,我们只需要调用它们的开启和关闭方法即可。
我们首先定义一个电灯类Light和电视类TV:
/**电灯类*/
public class Light {
public void on() {
System.out.println("灯亮了");
}
public void off() {
System.out.println("灯灭了");
}
}
/**电视机类*/
public class TV {
public void on() {
System.out.println("电视打开了");
}
public void off() {
System.out.println("电视关闭");
}
}
这两个类的实现很简单,其中只包含on()方法表示电器的开启和off()方法表示电器的关闭。
然后我们再来实现一个电风扇类CeilingFan:
/**电风扇类*/
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 + "电风扇调到了高档");
}
public void medium() {
speed = MEDIUM;
System.out.println(location + "电风扇调到了中档");
}
public void low() {
speed = LOW;
System.out.println(location + "电风扇调到了低档");
}
public void off() {
speed = OFF;
System.out.println(location + "电风扇关闭");
}
public int getSpeed() {
return speed;
}
}
电风扇类和电灯、电视机类有所区别,它除了有关闭方法off()之外,还有三个档位high、medium和low,这三个档位代表了电扇的三种风速,所以电扇类中也有三个方法表示将电扇调节到相应的档位。
2.3、第一种实现方式
现在我们就来分析一下如何实现需求,需求中提到,我们需要能够通过遥控器上的按钮来控制电器的开关,每个按钮代表的电器又是不同的,所以我们定义一个遥控器类,在这个遥控器类中定义一个List表示按钮控制的电器,然后我们可以定义一个开启方法和关闭方法,只需要判断指定按钮控制的电器是哪一个调用相应的方法即可,下面是我们基于此想法编写的遥控器类:
public class RemoteControl {
private List<Object> buttonList;
public RemoteControl() {
buttonList = new ArrayList<Object>();
buttonList.add(new Light());
buttonList.add(new TV());
buttonList.add(new CeilingFan("主卧"));
}
/**
* 按下开启按钮
* @param slot
*/
public void onButtonWasPushed(int slot) {
if(buttonList.get(slot) instanceof Light) {
((Light) buttonList.get(slot)).on();
} else if(buttonList.get(slot) instanceof TV) {
((TV) buttonList.get(slot)).on();
} else if(buttonList.get(slot) instanceof CeilingFan) {
((CeilingFan) buttonList.get(slot)).low();
}
}
/**
* 按下关闭按钮
* @param slot
*/
public void offButtonWasPushed(int slot) {
if(buttonList.get(slot) instanceof Light) {
((Light) buttonList.get(slot)).off();
} else if(buttonList.get(slot) instanceof TV) {
((TV) buttonList.get(slot)).off();
} else if(buttonList.get(slot) instanceof CeilingFan) {
((CeilingFan) buttonList.get(slot)).off();
}
}
}
在这个类中我们可以看到我们在初始化这个遥控器类的时候就初始化了遥控器类中的部分按钮,分别是电灯、 电视和主卧的电视,在按下启动按钮后我们会判断按下的是哪一排的启动按钮,然后使用if语句判断此按钮对应的电器是哪一种,最后调用电器的启动方法即可,按下关闭按钮的方法执行过程和启动按钮是类似的。
上面的这种实现方式确实也实现了我们的需求,但是是存在问题的:
- 首先在这段示例中我们需要在遥控器中初始化每一排按钮对应的电器,这样的话遥控器中每排按钮对应的电器在初始化的时候就已经定义好了,我们无法动态地向其中添加和删除电器,遥控器和电器之间是紧紧耦合在一起的,也就是说每次我们想要修改遥控器控制的电器都需要修改遥控器的代码;
- 另外我们需求中提到遥控器还需要有一个按钮来进行上一步的撤销操作,但是通过这种实现方式我们是无法进行撤销操作的。
我们可以通过命令模式来解决上面这些问题。
2.4、命令模式实现
那么命令模式该如何实现我们的需求呢?在命令模式的定义中我们讲到可以将请求封装成对象,也就是说我们不不再直接调用电器类,而是将电器的执行封装成一个对象,在这里我们先定义一个接口Command,它对外暴露了电器执行的接口:
public interface Command {
/**执行方法*/
public void execute();
/**撤销方法*/
public void undo();
}
在这个Command接口中我们定义了两个方法execute()和undo(),execute()方法表示某个电器命令的执行,undo()方法是撤销操作,其实就是execute()方法倒转过来,这个方法是用于遥控器撤销按钮操作的。
接下来我们定义一系列的命令,每一个命令都代表了某个电器某一步的执行操作,例如电灯打开命令表示电灯开启,电视的关闭命令表示电视关闭等等:
/**电灯打开命令*/
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();
}
}
/**电视打开命令*/
public class TVOnCommand implements Command {
TV tv;
public TVOnCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.on();
}
@Override
public void undo() {
tv.off();
}
}
/**电视关闭命令*/
public class TVOffCommand implements Command {
TV tv;
public TVOffCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.off();
}
@Override
public void undo() {
tv.on();
}
}
这些命令的实现都很简单,在execute()方法中调用相应电器的相应方法,而在undo()方法中调用相应电器相反的方法即可,接下来我们来看一下电扇命令的实现,因为电扇有三挡,对于撤销操作,我们需要能够记住上次电扇所处的档位,所以我们可以通过一个属性来表示电扇上一次所处的档位,在undo()方法中根据这个档位来调用相应的方法,下面是我写的电扇在高档转动的命令,其余档位的电扇转动命令都是一样的,在这里就不再赘述了:
public class CeilingFanHighCommand implements Command {
CeilingFan fan;
int prevSpeed;
public CeilingFanHighCommand(CeilingFan fan) {
this.fan = fan;
}
@Override
public void execute() {
prevSpeed = fan.getSpeed();
fan.high();
}
@Override
public void undo() {
if(prevSpeed == CeilingFan.HIGH) {
fan.high();
} else if(prevSpeed == CeilingFan.MEDIUM) {
fan.medium();
} else if(prevSpeed == CeilingFan.LOW) {
fan.low();
} else if(prevSpeed == CeilingFan.OFF) {
fan.off();
}
}
}
所有命令都介绍完了之后我们就可以实现遥控器类了:
public class RemoteControl {
Command[] onCommands;
Command[] offCommands;
Command undoCommand;
public RemoteControl() {
onCommands = new Command[7];
offCommands = new Command[7];
for(int i = 0; i < 7; i++) {
onCommands[i] = new NoCommand();
offCommands[i] = new NoCommand();
}
undoCommand = new 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 undo() {
undoCommand.undo();
}
}
在这个遥控器类中我们定义了三个属性,分别代表7个开启按钮、7个关闭按钮和一个撤销按钮,我们在构造函数中将这些按钮进行初始化,可以看到我们使用的是一个叫做NoCommand类对它们进行初始化的,其实这个NoCommand不执行任何操作,它只是实现了Command接口而已,这样做可以防止命令出现null的情况。在这个遥控器类中我们只是初始化了这些按钮,但是每个按钮对应哪个命令我们没有指定,而是通过一个setCommand()的方法动态指定的。在执行开启和关闭操作的方法中我们不再需要判断按钮对应哪个电器,而是直接执行这个命令即可,同时在开启和关闭方法中我们将正在执行的命令赋值给撤销按钮,这样当需要执行撤销方法的时候,我们只需要执行对应命令的undo()方法即可。
上面的这种实现方式有效地解决了我们之前的问题,遥控器类和电器类不再紧耦合,遥控器类控制的电器可以通过Command接口来动态地指定,我们也可以动态地添加和删除按钮的命令。另外,我们还能够执行撤销操作。
我们通过一个main方法来测试一下我们之前写的功能:
public static void main(String[] args) {
Light light = new Light();
TV tv = new TV();
CeilingFan fan = new CeilingFan("主卧");
LightOnCommand lightOnCommand = new LightOnCommand(light);
LightOffCommand lightOffCommand = new LightOffCommand(light);
TVOnCommand tvOnCommand = new TVOnCommand(tv);
TVOffCommand tvOffCommand = new TVOffCommand(tv);
CeilingFanHighCommand fanHighCommand = new CeilingFanHighCommand(fan);
CeilingFanMediumCommand fanMediumCommand = new CeilingFanMediumCommand(fan);
CeilingFanLowCommand fanLowCommand = new CeilingFanLowCommand(fan);
CeilingFanOffCommand fanOffCommand = new CeilingFanOffCommand(fan);
RemoteControl remoteControl = new RemoteControl();
remoteControl.setCommand(0, lightOnCommand, lightOffCommand);
remoteControl.setCommand(1, tvOnCommand, tvOffCommand);
remoteControl.setCommand(2, fanHighCommand, fanOffCommand);
remoteControl.setCommand(3, fanMediumCommand, fanOffCommand);
remoteControl.setCommand(4, fanLowCommand, fanOffCommand);
remoteControl.onButtonWasPushed(0);
remoteControl.onButtonWasPushed(2);
remoteControl.undo();
remoteControl.onButtonWasPushed(1);
remoteControl.undo();
remoteControl.offButtonWasPushed(0);
remoteControl.onButtonWasPushed(3);
remoteControl.onButtonWasPushed(4);
remoteControl.undo();
}
我们设定了五个按钮分别表示电灯、电视、电扇的高档、中档和低档,然后执行相应的操作,执行结果如下:
灯亮了
主卧电风扇调到了高档
主卧电风扇关闭
电视打开了
电视关闭
灯灭了
主卧电风扇调到了中档
主卧电风扇调到了低档
主卧电风扇调到了中档
可以看到,命令模式很好的实现了我们的需求。
最后我们来看一下命令模式的类图(类图摘自《Head First设计模式》):
命令模式是由几部分组成的:
- Client(客户):它是负责创建具体的命令和接收者的,类似于我们的main方法所在的类;
- Invoker(调用者):它持有命令对象,并会调用命令对象的execute()方法来将请求付诸实行,类似于我们的遥控器类RemoteControl;
- Command:它为所有的命令声明了一个接口,调用命令对象的execute()方法就可以让接收者执行相关操作,另外它还有一个undo()方法用于撤销操作;
- ConcreteCommand:它是Command接口的实现,它定义了动作和接收者之间的绑定关系,调用者只要调用execute()就可以发出请求,然后由它来调用接收者的一个或多个动作,类似于我们实例中每一个Command的实现;
- Receiver(接收者):只有接收者知道如何进行操作,任何类都和当接收者,就类似于我们实例中的每个电器。
3、总结
通过上面的介绍,我们对命令模式有了一定的了解,命令模式能够将发出请求的对象和执行请求的对象解耦,被解耦的两者之间是通过命令对象进行沟通的。命令模式的一种简单扩展就是宏命令,就是允许调用多个命令,宏命令也支持撤销操作,通常命令可以用来实现日志和事务系统。但是命令模式也有缺点,使用命令模式的时候可能会导致出现过多的命令,在上面的实例的main方法中我们需要为每一个操作都定义一个命令,这无疑是增加了代码的复杂度。