首先你有一个遥控器,什么样的呢?它有七个插槽,可以插上七种不同的装置,每个插槽对应两个按钮,分别对插槽的装置进行开关操作。如下图:
现在你需要设计遥控器的API,使得遥控器可以接入某厂商的设备装置需要插到插槽上的设备的类如下图:
类不少,而且以后还会增加,所以设计一个复用性和可扩展性高的遥控器API变得十分迫切。
我们看到,厂商提供的设备类中除了有on和off方法外,还有setDirection和setVolume等等,根据之前设计模式提到的原则,隐约知道遥控器不需要知道这些类的设计具体实现,这样才可以满足扩展性要求,但是不知道具体实现,遥控器又如何完成对设备的操作呢?
这涉及到将动作的请求者和动作执行者解耦的问题,有一个方法,你可以给遥控器按钮绑定对应的命令,当按钮按A下时,它只是执行了命令A,而命令A干啥按钮并不需要知道,而具体设备的操作由命令A确定。这种方式叫做命令模式。
让我们通过一个常见的情景来认识命令模式:
在餐厅点餐工作流程中,顾客将写好的订单看做一个命令,服务员接受了这个命令,然后他把订单(命令)交给厨师,厨师再根据订单(命令)去做菜。在这个情景中,顾客是动作请求者,厨师是动作执行者,二者并没有接触(解耦),但是通过命令的传递完成了整个点菜流程。
回到遥控器,装置设备就犹如厨师,遥控者就是顾客,遥控器就类似服务员,而我们接下来就要实现类似订单的命令。看下面如何实现第一个命令对象:
首先是命令接口,所有的命令都是实现该接口:
接下来是一个打开和关闭电灯的命令:
接下来是遥控器类:
setCommand类似于将订单交给服务员,而buttonWashed则类似于将订单交给厨师做菜。
客户端程序:
看,Light就是厨师,on和off相当于做菜行为,LightXXCommand就是订单。通过setCommand将电灯操作命令传给遥控器,遥控器在某个时刻按下按钮调用命令的execute,而execute正是调用了Light的on和off方法,这样,遥控器和电灯的开关不就解耦了么,遥控器只知道它接受一个继承Command接口的类对象和按钮按下就调用命令的execute方法,其他的不用管,这样当引入其他装置设备时只要增加一个新的命令就行了。(是不是很high?)
该是官方定义的时候了:
命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也可以支持撤销操作。对象只暴露出一个execute方法,当此方法调用的时候接受者就执行相应的动作。从外面看,其他对象不知道哪个接受者进行了哪个动作,只知道如果调用execute方法,请求的目的就可以达到。
像遥控器的例子,遥控器不在乎拥有的什么命令对象,只要对象实现了Command接口就行了。
以下是命令模式的类图:
一句话总结,Invoker得到命令对象后,在某个时刻调用特定方法(比如遥控器按钮按下)触发命令对象调用execute方法,从而调用Receiver的相应方法(比如电灯的开或关)。
让我们开始遥控器的API设计吧~~
为每个按钮绑定命令:
然后是遥控器代码:
用两个数组分别表示开和关两排按钮,数组下标表示按钮位置。注意到构造方法中为两个数组初始化为noCommand,noCommand是什么呢?它是一个execute方法为空的实现Command接口的类,可以叫做一个空对象,它的对象没有任何实现真正意义上的功能,它的作用是确保每个插槽都有命令,以免在插槽对应按钮被按下的时候需要对插槽是否绑定命令进行判断,容易出现调用前忘记做判断的错误。
再看命令类,举一个比电灯复杂的例子,比如音响,方法稍微多了点:
客户端代码,测试遥控器:
看,遥控器只知道哪个插槽设置了什么命令,至于命令是什么,命令的接受者,它都一概不管。以后要更换插槽的控制设备,这与遥控器无关,只要增加新的命令,再绑定到遥控器按钮即可。
对了,还有命令的撤销的实现,先实现撤销最后一步操作的功能。其实很简单,只要在相应的命令接口中添加一个undo方法就行了,undo方法执行的是和execute相反的操作。
相应的命令类(例如电灯):
当然电灯关的命令就是让电灯开命令的execute方法的语句和undo方法语句调换位置就行了。
现在为遥控器代码添加了撤销最后一步的功能:
看到多了个undoCommand的对象,它的作用是记录最后一个命令操作。当撤销按钮按下的时候便调用undoCommand的undo方法。
我们还可以封装一个命令,让其可以执行多个命令:
使用数组将命令对象们存起来,在调用execute的时候执行所有命令的execute方法。这样调用一次方法就可以一次性执行所有的命令,而且可以根据需要安排命令的顺序。
总的来说,命令模式最大的优点就是将动作执行者和动作请求者解耦,常运用于对请求排队或者记录请求日志以及支持撤销操作。