命令模式
命令模式将“请求”封装为命令对象,不同的请求或者请求的列表可用来参数化其它对象,可以支持命令的撤销、重做的功能,命令可以包装多个其它命令,形成宏命令,该模式在GOF95中分类为行为模式。
- 命令请求者只依赖于抽象命令接口
- 命令接口具有唯一的方法,并且不需要传递参数
- 如果要支持撤销,则命令接口具有两个方法,另外一个是 undo()
- 支持撤销的时候, Invoker 需要知道它最后一次执行的是哪个命令;如果要支持多步撤销,则需要记住多个命令及其执行顺序
- 具体命令中封装了接收者
- 客户端负责创建合适的接收者、命令
命令模式的优点:
- 将动作的发出者与执行者解耦,动作发出者不知道是谁执行了它的命令,更不需要知道执行者的接口
- 更加动态的控制,通过对请求进行封装,可以动态的对它进行参数化、队列化、日志化
- 很自然的宏命令——命令的组合
命令模式的缺点:
- 可能导致系统中出现过多的具体命令类
命令模式的适用场景:
- 如果需要抽象出需要执行的动作,并参数化之
- 如果需要在不同的时刻指定、排列和执行请求
- 如果需要支持撤销、重做操作
- 如果需要在系统崩溃恢复后,将所有操作重新执行一遍,适合于事物系统,命令模式的另一个别名就是Transaction模式
有时候家里的空调遥控器坏了,我们会到电器店去买一个万能遥控器,回来设置一下就能使用了,那么如何设计一个万能遥控器呢?一个可扩展性差的实现方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//抽象空调类 public abstract class Conditioner{} //不同厂家定义的信号格式是不一样的,提供了不同的接口 public class HaierConditioner extends Conditioner { //发送1000红外信号,打开海尔空调 public void sendInfraredSingal_1000(){} } public class DaikinConditioner extends Conditioner{ //发送蓝牙2100信号,打开大金空调 public void sendBluetoothSingal_2100(){} } //万能遥控器面板 public class UniversalRemoteControlPanel{ private Conditioner conditioner; public void onButton1Pressed(){ if ( conditioner instanceof HaierConditioner ) ( (HaierConditioner) conditioner ).sendInfraredSingal_1000(); else if(conditioner instanceof DaikinConditioner) ( (DaikinConditioner) conditioner ).sendBluetoothSingal_2100(); else if(...) ; } } |
上述代码通过if-else结构,依据空调厂商调用不同的接口来实现打开空调的功能,这种方式有以下缺陷:
- 遥控器面板代码混入了空调厂商的私有逻辑,违反了单一职责原则,同时造成了紧耦合
- 由于每个厂商的接口都不一样,代码会随着支持厂商的增加不断膨胀,无法关闭修改
仔细思考一下可以发现,万能遥控器面板本身的职责就是发出开关、模式、温度调节等指令,至于如何对这些指令作出响应、处理则不关心,这恰恰是命令模式的经典适用场景,为此我们对上述实现进行改造,引入命令接口——通讯模块:
1 2 3 4 |
//通讯模块 public interface CommModule{ void signal(); //发送信号 } |
大金通讯模块的实现:
1 2 3 4 5 6 |
public class DaikinOpenCommModule implements CommModule{ private DaikinConditioner conditioner; //命令接口一般都要包裹一个实际的命令执行者,以响应请求 public void signal(){ conditioner.sendBluetoothSingal_2100(); } } |
通讯模块具有一致性的接口,遥控器面板只调用这一个接口,发出指令,不再和空调厂商有任何瓜葛:
1 2 3 4 5 6 |
public class UniversalRemoteControlPanel{ private CommModule openModule; public void onButton1Pressed(){ openModule.signal(); //只负责发出指令 } } |
使用万能遥控器的样例代码:
1 2 3 4 5 6 |
UniversalRemoteControlPanel cp = new UniversalRemoteControlPanel(); //我们使用大金空调 DaikinConditioner conditioner = checkConditioner(); DaikinOpenCommModule module = new DaikinOpenCommModule( conditioner ); cp.setOpenModule( module ); //加载模块 cp.onButton1Pressed();//按下按钮,打开空调 |
如果我们要支持更多的功能,例如关闭、设置运行模式,只需要在遥控器面板中增加新的通讯模块成员变量,并进行相应的开发即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class UniversalRemoteControlPanel{ //这三个命令即对应所谓“请求的列表” private CommModule openModule; private CommModule closeModule; private CommModule switchModeModule; public void onButton1Pressed(){ openModule.signal(); } public void onButton2Pressed(){ closeModule.signal(); } public void onButton3Pressed(){ switchModeModule.signal(); } } |
当然,也可以重新设计通讯模块的接口:
1 2 3 4 5 6 |
public interface CommModule{ void open(); //打开空调 void close();//关闭空调 void switchMode();//切换空调模式 void raiseTemperature();//升高设定温度 } |
这样就和命令模式原本的样子渐行渐远了, 但是学习设计模式最重要的一点就是不能生搬硬套呀!
好了,我们现在做一个具有“撤销”功能的遥控器,这个想法挺有创新性的!如前面所属,撤销功能的关键就是扩展命令接口,提供撤销方法,同时请求者需要记录命令执行的历史记录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public interface CommModule{ void execute(); //执行指令 void undo(); //撤销指令 } //大金温度调节通讯模块 public class DaikinRaiseTemperatureCommModule implements CommModule{ private DaikinConditioner conditioner; private int originalTemperature; //在命令中记录原先的温度设定 public void execute(){ originalTemperature = conditioner.sendBluetoothSingal_1005(); //获取空调当前温度 conditioner.sendBluetoothSingal_2009( originalTemperature + 1 ); //设定温度 } public void undo(){ conditioner.sendBluetoothSingal_2009( originalTemperature );//设定温度 } } public class UniversalRemoteControlPanel{ private CommModule module; private List history = new ArrayList(); private int undoIndex = -1; //调节温度按钮 public void onButton4Pressed() { module.execute(); history.add( module ); } //撤销按钮 public void onUndoButtonPressed() { history.get( nextUndoIndex() ).undo(); } //重做按钮 public void onRedoButtonPressed(){ history.get( nextRedoIndex() ).execute(); } } |
使用带撤销功能的万能遥控器的样例代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
UniversalRemoteControlPanel cp = new UniversalRemoteControlPanel(); DaikinConditioner conditioner = new DaikinConditioner(); DaikinRaiseTemperatureCommModule module = new DaikinRaiseTemperatureCommModule(conditioner); cp.setModule( module ); //升高三度 cp.onButton4Pressed(); cp.onButton4Pressed(); cp.onButton4Pressed(); //撤销一次 cp.onUndoButtonPressed(); //重做一次 cp.onRedoButtonPressed(); |
能不能按一下就可以设置为制冷模式、25摄氏度、开启扫风功能呢?一个个按钮去操作很麻烦。在命令模式中,包装多个命令并批量执行的命令被称为“宏命令”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class MacroCommModule implements CommModule { private List modules; public void execute(){ for ( CommModule module : modules ){ module.execute(); } } public void undo(){ for ( CommModule module : reverse( modules ) ){ module.undo(); } } } |
Java中Thread与Runnable的关系就是Command模式中Invoker与Command的关系:
Thread会在其start()方法被调用后,发动新的线程,并执行其run()方法, 而run()方法直接转调命令对象Runnable的run()方法。
QT中的Undo Framework是命令模式的实现,用于支持撤销/重做功能。在QT应用中,每个编辑操作可以被抽象为Command的实例,并且被保存在命令栈中,每个Command知道如何撤销修改并把程序还原到前一个状态。通过向下/上遍历命令栈,可以实现撤销/重做。
上面类图中QUndoCommand相当于Command角色,多个命令可以被存放到QUndoStack中,多个QUndoStack可以形成一个组。QUndoView是一个可以显示命令栈内容的小器件。
- 智能命令:命令本身已经把Receiver装配好,Client直接使用,不需要装配
- Client经常与Invoker融合为一个角色
- 回调:Invoker不再持有Command作为成员变量,而是声明自己的方法入参为Command,在方法体中回调Command.exec(),此时Invoker角色已经退化为一个方法