设计模式之命令模式
前言
假设,小明去一个餐厅吃饭,首先,他会把服务员叫过来,告诉服务员自己想要吃什么,服务员将小明创建的订单拿到后台窗口,告诉厨师,订单来了,厨师看到订单之后,就开始做相应的食物。
这是一个很常见的生活细节,那么问题来了,为什么小明不直接去跟厨师打交道,直接告诉厨师自己需要什么呢?因为有这样几个问题:
- 小明并不知道厨师在哪儿
- 小明找到厨师之后会一个不小心了解到了一些不该看到的后台场景(比如:脏、乱、差)
- 但很多人人都来找厨师的时候,会把后台弄得很乱
- 如果新增加小明以前从来没有接触过的厨师,那估计又得有很多麻烦了
- 。。。。
那么,这些问题的关键在于什么?关键在于小明的发出的请求(即动作的请求者)和厨师完成小明的请求(即动作的执行者)之间有很复杂的关系,这就是耦合性很高,因此产生了很多问题。
因此,在实际生活中,中间是有服务员的,服务员的角色就将小明(动作的请求者)和厨师(动作的执行者)进行了完全解耦。小明只需要将自己想要的订单告诉服务员(createOrder),他并不关心具体是怎么做的,服务员拿到订单后(takeOrder)就告诉厨师有订单来了(orderUp),然后厨师并不关注是谁点了这些吃的,他只负责做就行了。其中,服务员就相当于命令的管理者。
实现这样的命令模式
依据我们上面的分析,我们得到一下几个元素:
- 谁使用遥控器 —> 用户(Client)
- 谁执行遥控器的命令:来自厂商的具体家电的操作的类 —> 厨师(Receiver)
- 谁来管理这些命令:遥控器本身 —> 服务员(Waiter)
- 由用户传递给遥控器,再由遥控器传递给具体的家电之间的信号 —> 命令(Command)
首先分析动作请求者合动作执行者之间传递的命令
客户告诉服务员,服务员告诉厨师,她们只做了一件事,那就是执行这个命令,只是服务员执行命令是orderUp,即通知厨师,厨师收到命令之后,就做他该做的事了,所以,在命令里面,只需要包含一个执行的方法即可:
public interface Command {
public void execute();
}
在具体的命令中,比如开灯命令,因为在这个命令里面的execute会将执行权交给最终执行者,即具体的家电类,所以,在具体命令的实现中,需要引用相应的具体的家电类:
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light){
this.light = light;
}
public void execute() {
light.on();
}
}
最终执行开灯的是Light这个类。到这里,一个开灯的命令就写好了,而且该命令已经关联了具体的动作执行者。但是,具体执行者是Light,我们还没有Light这个家电的具体操作类(即厨师),所以添加Light的具体操作(厨师做食物的具体方法):
public class Light {
private String where = "";
public Light(String where){
this.where = where;
}
public void on(){
System.out.println(where+"灯开了 ");
}
}
其中的where成员变量只是为了表示是哪里的灯,在此处做讲解阔以完全去掉where变量。到目前位置,我们已经完成了上述第二个合第三个任务了,接下来,就需要关联上动作的申请者。但是,我们知道,仅仅一个命令是不能直接被调用的,它必须衣服在一个管理这些命令的对象上,这个时候就是遥控器这个类出场了。
再分析这些命令的管理者:遥控器
单独的命令是无法对上用户请求的,用户下单之后没用服务员,是无法告知厨师进行执行命令的。所以,需要由服务员来完成命令的装配操作,服务员需要一方面提供给顾客进行下单的接口,还得将这个命令告诉厨师。
通过分析,在遥控器这样的对象中一定引用一个命令,而且还需要将用户的想要开灯的命令(体现在用户选择按开灯按钮,但是还没按)变现在遥控器当中,即将遥控器中的命令设置为开灯命令(现在就等用户按下去了),并且还需要将来自用户的操作(比如按下遥控器的按钮)转变为命令并且让改命令的执行者(即相应的家电,在餐厅例子中就是厨师)执行相应的命令。通过以上分析,我们得到一下的遥控器类的代码:
public class SimpleRemoteControl {
Command command;
public void setCommand(Command command){
this.command = command;
}
public void buttonPressed(){
if(command==null){
System.out.println("您还没有选择按哪一个按钮");
}else {
command.execute();
}
}
}
其中:
- setCommand() 就是设置用户想要进行的命令是什么(在餐厅例子中就是用户想要点餐,而不是想要问洗手间在哪儿),此处就是想开灯。
- buttonPressed() 即用户按下了按钮,遥控器就需要通知相应的家电执行相应的命令了(在餐厅例子中就是用户确定下单了,然后告诉了服务员,服务员将订单给厨师进行执行)。
现在的遥控器具备了管理一组命令的功能,一方面等待被用户按下,另一方面将命令告知具体的执行类并执行,此时就已经完成了任务的第四条,就剩最后客户的实现了。
分析客户
在上面的两个步骤中,我们已经准备好了遥控器,并在遥控器中安装好了命令,而且命令中设置了具体的执行者,那接下来就是去使用这个遥控器了。进入餐厅,想要获得服务,其实是依靠在整个餐厅的服务流程来的,所以,用户想获得食物,前提是,他进入餐厅之后就拥有了相应的获取服务的能力(比如召唤服务员,下单之类),而不是没办法获得服务员的服务(比如今天餐厅打烊)。这是什么意思呢?
假设一个用户想要使用遥控器,那他必然拥有遥控器,必然拥有遥控器所遥控的灯,所以,在用户类中,一方面的任务是使用,另一方面,就是在使用之前先获取到相应的服务(即相应的类)。这样分析,就得到,有一个遥控器类,还得有一个可以下命令的命令类,这个命令类需要有人执行,所以还得有个执行命令的家电类,并且将他们进行组装:
public class TestSimpleRemoteControl {
public static void main(String[] args) {
SimpleRemoteControl src = new SimpleRemoteControl();
Light light =new Light("");
Command command =new LightOnCommand(light);//在命令中安装执行者
src.setCommand(command);//在遥控器中安装命令
src.buttonPressed();
}
}
以上我们便完成了所有的任务了,此时一个简要的遥控器的功能已经完成,在用户想要开灯的时候,设置好开灯命令,并且按下开灯按钮即可。其实,以上便是一个简单的命令模式的具体实现,从分析到具体代码的实现,再增加新的操作合命令,就按照这个模式进行扩展即可。
简单扩展一个新命令
那么,尝试一下一个新的命令吧,实现GarageDoorOpenCommand,让车库的门打开:
public class GarageDoorOpenCommand implements Command{
GarageDoor garageDoor;
public GarageDoorOpenCommand(GarageDoor door){
garageDoor = door;
}
public void execute(){
garageDoor.up();
}
}
同样都是实现了Command接口,并且引用了相应的具体的动作执行者的类,因此,同样需要具体动作执行者类的实现(简单增加了几个动作执行者类的操作):
public class GarageDoor {
public void up(){
System.out.println("门打开了");
}
public void down(){
System.out.println("门关上了");
}
public void stop(){
System.out.println("门停住了");
}
public void lighton(){
System.out.println("车库的灯打开了");
}
public void lightoff(){
System.out.println("车库的灯关上了");
}
}
接下来只需要告诉用户的所拥有的命令有哪些,并且阔以在遥控器上按下相应的按钮,因此,在上面的客户端类中增加相应的代码即可:
public class TestSimpleRemoteControl {
public static void main(String[] args) {
SimpleRemoteControl src = new SimpleRemoteControl();
Light light =new Light("");
Command command =new LightOnCommand(light);
src.setCommand(command);
src.buttonPressed();
GarageDoor door =new GarageDoor();
command = new GarageDoorOpenCommand(door);
src.setCommand(command);
src.buttonPressed();
}
}
以下是上面代码的运行结果:
命令模式
接下来就是正式进入命令模式的介绍中了,通过上面的分析,应该会比较好理解了相应的定义。
命令模式 将"请求"封装成对象(即Command对象),以便使用不同的请求、队列或者日志来参数化其他对象(即设置不同的命令和命令中设置不同的执行者对象),命令模式还支持撤销的操作(稍后介绍)。
实现完整版的遥控器
有了基本的命令模式的设计思路之后,我们就阔以实现一个完整的遥控器的类了。先假设是这样的场景,有7个命令插口,对应这分别有开和关两个按钮,同时最底下还支持一个UNDO按钮,用来撤销上一步操作。
明确了需求,我们就开始分析需要完成的任务(暂时不考虑UNDO按钮),首先,我们有7个插槽,对应的有14个按钮,即14个命令,即每一个Command对应的与OnCommand和OffCommand两个命令,即对于一个插槽来说,我们需要设置两个命令,于是产生了类似下面的setCommand:
public void setCommand(int slot ,Command onCommand ,Command offCommand){
...
}
其中的slot表示是第几个插槽对应的命令。但是,问题来了,这个onCommand和offCommand应该给谁设置了,或者说应该存放在哪儿了?
换个角度思考,我们应该是不想唯美个按钮设置一个按下的操作,那样似乎太不明智了,我们应该只需要知道当前是第几个插槽对应该哪一个按钮被按下就行了,也就是类似于:
public void btnOnPressed(int slot){
...
}
public void btnOffPressed(int slot){
...
}
那在这两个方法里面怎么调用了?对了,就是通过slot获取命令,然后执行命令的execute方法就行了!那么,我就需要存放on命令的数组合存放off命令的数组(当然,也阔以把这两个数组合成一个二维的),所以就有了类似于这样的成员变量:
private Command[] onCommands;
private Command[] offCommands;
由此,我们几乎已经该清楚了应该怎么完成上面的代码了,完整的应该和下面的差不多:
public void setCommand(int slot ,Command onCommand ,Command offCommand){
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void btnOnPressed(int slot){
onCommands[slot].execute();
}
public void btnOffPressed(int slot){
offCommands[slot].execute();
}
到这里,基本上阔以说完成的差不多了,但是,有一点需要注意,setCommand这个方法并不一定所有的插槽对应的按钮都会执行,那么,问题来了,要是有的插槽没有设置(即功能暂时保留),那会出现什么?是的,空指针异常!原因也很简单,那就是onCommand和offCommand数组并没有初始化,此时如果还没有通过setCommand进行赋值的话,就会出现空指针了。那么解决方法呢?
为了避免出现空指针,我们阔以在使用的时候来个判断,如果为空,不使用就行了,于是就得到了:
方法一:
在按钮按下的方法中进行如下的修改:
public void btnOnPressed(int slot){
if(onCommands[slot]!=null) {
onCommands[slot].execute();
}
}
这样的话,就不会出现命令为空指针的情况了!但是,这样的话在每一次使用命令的时候就需要进行判断一次是否为空,显然,这不是我们想要的,那么,有什么更好的解决方案了?这样就出现了下面的方法二。
方法二:
这里就引入了一个新对象,NoCommand也是实现了Command接口,但是这个类里面什么都不做,这就称为空对象。
当我们在处理某个类,然后该类肯呢个出现空指针的问题,这个时候,就阔以将处理Null的责任交给空对象类。比如,在设置遥控器的出厂初始化的功能的时候,阔能有的命令暂时并没有,就阔以将该命令初始化为空对象,这个对象什么都不做,且不会出现空指针。
Tips:空对象在其他的设计模式中也会有使用,或者说其本身也是一种设计模式。
这个时候就回到我们上面已经实现的代码中,我们发现此时的onCommand[]和offCommand[]都还没有进行初始化,那么根据我们上面的思路,就阔以的到下面的初始化方式:
private static final int SWITCH_NUM =7;//命令的个数
public RemoteControl(){
onCommands = new Command[SWITCH_NUM];
offCommands = new Command[SWITCH_NUM];
Command noCommand = new NoCommand();
for (int i=0; i<SWITCH_NUM;i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
到这里,我们的完整版的遥控器也实现了,回过头来整理一下,就得到了下面的完整的代码:
public class RemoteControl {
private Command[] onCommands;
private Command[] offCommands;
private static final int SWITCH_NUM =7;
public RemoteControl(){
onCommands = new Command[SWITCH_NUM];
offCommands = new Command[SWITCH_NUM];
Command noCommand = new NoCommand();
for (int i=0; i<SWITCH_NUM;i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
public void setCommand(int slot ,Command onCommand ,Command offCommand){
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void btnOnPressed(int slot){
onCommands[slot].execute();
}
public void btnOffPressed(int slot){
offCommands[slot].execute();
}
}
实现undo操作
虽然完成了上面的任务已经差不多了,但是,我们不要忘了,前面还遗留了一个问题,那就是,如何实现撤销undo的功能呢?继续来分析问题!
思考一下,如果要实现撤销操作,得有一个前提,那就是知道刚刚执行完的命令是什么,所以,就像设置临时变量一样,设置一个lastCommand来记录用户上一次执行完的操作,然后,按下撤销按钮之后,就相当于执行一下lastCommand相应的命令。有了这个思路,则开始修改代码!等等,首先应该是给command增加一个undo的命令才对(不然按下按钮之后执行啥呢?)。
public interface Command {
public void execute();
public void undo();
}
同样的需要在实现了Command的接口的类进行实现相应的undo的方法,但是怎么实现了?我们考虑一下,之前实现了LightOnComamnd命令,当改命令调用execute方法的时候就会执行light.on()方法,还记得吗?那假设灯已经打开了,我要撤销灯打开的操作,需要怎么做?对的,就是把灯关了就行了!
public void undo() {
light.off();
}
就是这么简单,当然,其他的命令阔以按照这个思路相应的实现就好了!但是,这个仅仅是对命令做的修改,那遥控器应该怎么做呢?接着上面分析的,在遥控器中维持一个命令比变量用来记录用户刚执行的命令,当用户再按下undo的按钮的时候就是调用刚刚用户执行的命令的undo方法就行了!修改之后的遥控器代码如下所示:
public class RemoteControlWithUndo {
private Command[] onCommands;
private Command[] offCommands;
private Command lastCommand;
private static final int SWITCH_NUM =7;
public RemoteControlWithUndo(){
onCommands = new Command[SWITCH_NUM];
offCommands = new Command[SWITCH_NUM];
Command noCommand = new NoCommand();
for (int i=0; i<SWITCH_NUM;i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
lastCommand = noCommand;
}
public void setCommand(int slot ,Command onCommand ,Command offCommand){
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void btnOnPressed(int slot){
onCommands[slot].execute();
lastCommand = onCommands[slot];
}
public void btnOffPressed(int slot){
offCommands[slot].execute();
lastCommand = offCommands[slot];
}
public void undoPressed() {
lastCommand.undo();
}
}
到这里为止,我们就是已经实现了对于LightOnCommand这个命令的undo操作,其他的命令的undo命令根据这个样子实现即可。测试的方法也是在之前的测试用例中加入按下undo的操作就行了!
总结
以上就是命令的基本使用方法,当然,我们在实际应用中还会为了满足某些特定场景的而设置一些命令宏,当用户按下按钮之后,执行一系列的命令。通过命令模式还阔以用来实现队列请求、日志请求等具体实例。