设计模式(一):命令模式(1)——基本的命令模式 设计模式(一):命令模式(1)——基本的命令模式设计模式(一):命令模式(2)——命令模式扩展之宏命令设计模式(一):命令模式(3)——命令模式扩展之队列请求设计模式(一):命令模式(4)——命令模式扩展之日志请求
前言
命令模式的讲解分为四篇:
一、生活中的命令模式
1.案例
如果将命令模式反映到生活中,遥控器无疑是一个很好的例子。假如我们有如下一个遥控器
这个遥控器有三个插槽(编号为0,1,2),每个插槽对应着要操作的一个电器,插槽所控制的电器这里设置的分别是卧室灯、空调、冰箱,这些电器是可以换成其他的电器的。每个插槽分别对应一个打开按钮和一个关闭按钮(on和off)用于打开和关闭相应的电器,另外还有一个撤销按钮(undo)用于撤销上一步所进行的操作(如果我按下0号位上的on按钮,那么电灯将会打开,再按下undo按钮,电灯就会熄灭)。基于这个条件来给遥控器进行编程。
让我们从遥控器工作的流程来分析一下。当我们按下0号插槽的on按钮时,一个打开电灯的命令就会被传递到插槽之中,插槽此时就会执行打开电灯的命令将电灯打开。因为插槽所控制的电器是可以改变的,0号插槽现在用来控制卧室灯的开关,以后可能用来控制电饭煲的开关。所以,遥控器是不会关心电器的细节的。这就要求我们将遥控器和电器进行解耦。为了将遥控器和具体的电器进行解耦,那么我们可以将按钮对应的命令封装成对象,并借用命令对象实现遥控器和具体电器的解耦。让我们对着下面的图来理解一下。
1.给遥控器的每个按键设置一个命令(Command),比如途中给其中一个on按钮设置了LightOnCommand命令。其中LightOnCommand里面包含执行具体打开动作的电灯(电器)Light。
2.当按下遥控器的On请求打开电灯时,就将请求委托给了命令对象。以后直到电灯打开,所有细节都将和遥控器无关。此时已经实现了遥控器和电灯的解耦。
3.因为命令对象中持有电灯对象,命令对象直到如何去做,命令对象此时只需要调用电灯的on方法就可以打开电灯。
通过上面图和图的解释我们可以看到通过将请求封装成对象,实现了遥控器和电灯的解耦,以后如果插槽所对应的电器换成了电饭煲。当我们需要开启电饭煲时,我们只需要将LightOnCommand换成电饭煲打开对应的xxxOnCommand即可,遥控器我不需要修改任何个代码。至此为止,所有的on按钮和off按钮都已经完全实现,还剩一个undo按钮的功能没有实现。同样的我们也只需要给undo按钮分配一个命令就可以实现撤销功能,只不过这个撤销命令是需要遥控器的操作过程中进行记录的。
2.代码
下面将案例的代码实现一下。这里需要注意:因为具体代码中有Light(电灯),Refrigerator(冰箱),AirCondition(空调)三种电器,三种电器又各自对应开和关的命令。为了文章的简洁,下面的代码将只包含电灯Light和其对应的开和关命令,其他电器和其对应的开和关命令被省略。想看具体的代码可以到github:https://github.com/wutianqi/desin-patterns/tree/master/design-pattern/src/main/java/com/wutqi/p1/command_pattern/p1/basic
**************Light**************
/** * 电灯 * @author wuqi * @Date 2019/1/29 13:17 */ public class Light { public static final Integer ON = 1; public static final Integer OFF = 0; private Integer status = OFF; public void on(){ this.status = ON; System.out.println("the light is on..."); } public void off(){ this.status = OFF; System.out.println("the light is off..."); } public Integer getStatus(){ return this.status; } }
**************Command**************
/** * 命令接口 * @author wuqi * @Date 2019/1/29 13:33 */ public interface Command { /** * 执行命令 */ public void execute(); /** * 撤销命令 */ public void undo(); }
**************LightOnCommand**************
/** * 开灯命令 * @author wuqi * @Date 2019/1/29 13:36 */ public class LightOnCommand implements Command{ private Light light; private Integer preStatus; public LightOnCommand(Light light){ this.light = light; } @Override public void execute() { preStatus = light.getStatus(); light.on(); } @Override public void undo() { if(Light.ON.equals(preStatus)){ light.on(); } else { light.off(); } } }
**************LightOffCommand**************
/** * 关灯命令 * @author wuqi * @Date 2019/1/29 13:55 */ public class LightOffCommand implements Command { private Light light; private Integer preStatus; public LightOffCommand(Light light){ this.light = light; } @Override public void execute() { preStatus = light.getStatus(); light.off(); } @Override public void undo() { if(Light.ON.equals(preStatus)){ light.on(); } else { light.off(); } } }
**************LightOffCommand**************
/** * 无任何响应的命令 * @author wuqi * @Date 2019/1/29 14:14 */ public class NoCommand implements Command { @Override public void execute() { //不做任何事情 } @Override public void undo() { //不做任何事情 } }
**************RemoteControl**************
/** * 遥控器 * @author wuqi * @Date 2019/1/29 14:02 */ public class RemoteControl { /** * on按钮 */ private Command[] onCommands; /** * off按钮 */ private Command[] offCommands; /** * undo按钮 */ private Command undoCommand; /** * 最后一个命令 */ private Command lastCommand; public RemoteControl(){ //遥控器初始化时,将所有的按钮对应的命令设置成Nocommand,即按下时没有任何反应 NoCommand noCommand = new NoCommand(); onCommands = new Command[3]; offCommands = new Command[3]; for(int i=0;i<3;i++){ onCommands[i] = noCommand; offCommands[i] = noCommand; } undoCommand = noCommand; } /** * 设置按钮指令 * @param position * @param onCommand * @param offCommand */ public void setCommand(int position,Command onCommand, Command offCommand){ onCommands[position] = onCommand; offCommands[position] = offCommand; } /** * 选择按第几号on按钮 * @param position */ public void onButtonPushed(int position){ this.lastCommand = onCommands[position]; onCommands[position].execute(); } /** * 选择按第几号off按钮 * @param position */ public void offButtonPushed(int position){ this.lastCommand = offCommands[position]; offCommands[position].execute(); } /** * 撤销最后一次执行的命令 */ public void undo(){ lastCommand.undo(); } }
**************RemoteControlTest**************
/** * 测试遥控器 * @author wuqi * @Date 2019/1/29 14:20 */ public class RemoteControlTest { public static void main(String[] args) { //创建电器 Light livingRoomLight = new Light(); AirCondition airCondition = new AirCondition(); Refrigerator refrigerator = new Refrigerator(); //创建遥控器,并给遥控器的三个插槽对应的on和off按钮指定命令 LightOnCommand lightOnCommand = new LightOnCommand(livingRoomLight); LightOffCommand lightOffCommand = new LightOffCommand(livingRoomLight); AirConditionOnCommand airConditionOnCommand = new AirConditionOnCommand(airCondition); AirConditionOffCommand airConditionOffCommand = new AirConditionOffCommand(airCondition); RefrigeratorOnCommand refrigeratorOnCommand = new RefrigeratorOnCommand(refrigerator); RefrigeratorOffCommand refrigeratorOffCommand = new RefrigeratorOffCommand(refrigerator); RemoteControl remoteControl = new RemoteControl(); remoteControl.setCommand(0,lightOnCommand,lightOffCommand); remoteControl.setCommand(1,airConditionOnCommand,airConditionOffCommand); remoteControl.setCommand(2,refrigeratorOnCommand,refrigeratorOffCommand); //打开电灯 remoteControl.onButtonPushed(0); //关上电灯 remoteControl.offButtonPushed(0); //按下撤销键,再次开启电灯 remoteControl.undo(); //打开空调 remoteControl.onButtonPushed(1); //关上空调 remoteControl.offButtonPushed(1); //打开冰箱 remoteControl.onButtonPushed(2); //关闭冰箱 remoteControl.offButtonPushed(2); //按下撤销键,再次打开冰箱 remoteControl.undo(); } }
执行测试得到如下的结果,下面的结果也印证了我们的遥控器各个按钮可以正常的工作:
说明:1.上面代码中有一点是比较奇妙的,在初始化RemoteControl(遥控器)时,将onCommands和offCommands还有lastCommand全部设置成NoCommand。这样做的好处是,一开始各个按钮就可以按下,并且不会做任何事情,也避免了异常的抛出。
2.撤销命令代码里只是撤销了最后一步执行的命令,如果想撤销前面所有的命令,可以用Stack来存储执行的命令,依赖来撤销。
二、定义命令模式
说完生活中存在的命令模式,下面我们来看下设计模式中命令模式的定义。
1.命令模式的概念
将“请求”封装成对象,以遍使用不同的请求、队列或者日志来参数化其他的对象。命令模式也支持可撤销的操作。
2.命令模式概念解析
1.通过上面遥控器的例子,我们也知道了请求被封装成了对象。再看这个定义就是一个命令对象通过在特定接收者上绑定一组动作来封装一个请求。要达到这一点,命令对象将动作和接收者包装进对象中。这个对象只暴露出一个execute()方法,当此方法被调用的时候,接收者就会进行这些动作。从外面来看,其他对象不知道究竟哪个接收者进行了哪些动作,只知道如果调用execite()方法,请求的目的就能达到。
2.利用请求、队列或者日志来参数化其他对象。我们上面的例子中体现了用请求也就是命令来参数化对象。队列和日志是命令模式的一些扩展(本文中未涉及)。在遥控器中,我们用setCommands并传入命令对象数组来参数化遥控器对象。遥控器根本不需要知道具体的命令类型,它只需要知道这些是Command接口即可。
3.命令模式UML类图
4.深入理解命令模式
上面遥控器中的命令对象是一种“傻瓜”命令对象,也是我们应该尽量设计的,他只懂得调用一个接收者的一个行为。然而有许多聪明的命令对象会实现许多逻辑,直接完成一个请求。当然你可以设计聪明的命令对象,只是这样一来,调用者和接收者之间的解耦程度要比不上“傻瓜”命令对象的,而且,你也不能够把接收者当做参数传入给命令。实际操作时,很常见使用“聪明”命令对象,这也就是直接实现了请求,而不是将请求委托给接收者。
三、命令模式应用场景
通过上面的学习,我们也可以很直观的看到命令模式适合用在需要将请求调用者和请求的执行者进行解耦的场景。当你需要请求的撤销操作时也是可以使用命令模式的。
四、辩证看待命令模式
1.优点:命令模式可以将请求的调用者和请求的执行者进行解耦。
2.缺点:命令模式因为需要将命令封装成对象,所以每有一个命令就需要创建一个对象,这样造成命令对象这些小类特别多。
参考资料:《Head first in 设计模式》