Head First设计模式——命令模式
前言:命令模式我们平常可能会经常使用,如果我们不了解命令模式的结构和定义那么在使用的时候也不会将它对号入座。
举个例子:在winform开发的时候我们常常要用同一个界面来进行文件的下载,但是并不是所有地方都用同一个下载逻辑处理文件,然后下载界面却可以是同一个界面。
为了以后复用下载界面(下载显示,进度条等)我们常常将下载执行操作定义成一个接口,在具体使用的时候实现接口,将具体执行对象设置到下载界面。当下载按钮被按下的时候,就调用设置的具体执行对象(接收者)来执行下载的处理。
那接下来我们就看下命令模式的具体细节和实现,再回头想想我们平时什么时候不经意就使用到了命令模式,这样以后交流使用专业的术语不仅能装还能用。
1、遥控器应用场景
HeadFirst设计模式一书中以遥控器为例实现命令模式,以餐馆点餐讲解命令模式的对象和结构。为了逻辑清晰我们不混合两种讲解方式,只以遥控器为例讲解。
现在需求是有一个遥控器,遥控器上面有控制各种电器的开关,而开关的执行控制电器是由各个厂家开发的设备(对象)插入到对应开关位置的卡槽里面,基于这些条件我们来实现遥控器系统。
简单粗暴的解决方案可以对开关做一个标识,当某个开关被按下时根据开关类型进行if判断。形如 if slot1==Light ,then light.on(), else if slot1==Tv then tv.on() 这种代码将出现一堆,对于以后增加减少开关或者更换开关都是比较糟糕的。而对于设计遥控器类来说我们应该让遥控器代码尽量保持简单,而不用去关心具体厂商类怎么执行。所以我们应该将执行封装在一个命令对象里中,那么我们就试着一步步实现遥控器。
首先我们为命令对象定义一个统一的接。
接口只有一个简单的execute执行命令方法。
public interface Command { //执行命令的方法 public void execute(); }
接下来我们实现一个打开电灯的命令
public class Light { public void on() { Console.WriteLine("打开电灯"); } public void off() { Console.WriteLine("关闭电灯"); } } public class LightOnCommand : Command { Light light; public LightOnCommand(Light light) { this.light = light; } public void execute() { light.on(); } }
为了简单我们假设遥控器只有一个开关,实现遥控器。
public class SimpleRemoteControl { //卡槽 Command slot; public void setCommand(Command command) { slot = command; } //按下开关 public void ButtonWasPressed() { slot.execute(); } }
测试
static void Main(string[] args) { SimpleRemoteControl remoteControl = new SimpleRemoteControl(); //厂商提供的电灯类,命令的接收者 Light light = new Light(); //我们封装的命令对象,设置接收者 LightOnCommand lightOnCommand = new LightOnCommand(light); //设置遥控器开关对应的命令对象 remoteControl.setCommand(lightOnCommand); remoteControl.ButtonWasPressed(); Console.ReadKey(); }
2、命令模式、类图
通过上面的例子我们已经使用了命令模式来实现一个简单的遥控器,再回顾【前言】我们说的界面下载文件按钮操作是不是就是一个典型的可以使用命令模式的应用场景。
只是有一点我们可能不会有什么其他厂商设计好的执行类,我们也许直接就在继承接口的命令对象中实现execute的逻辑,而不用再调用其他接收者执行。
这就是“聪明”命令对象,上面我们实现的是“傻瓜”命令对象。这个稍后再说,我们先看命令模式定义和画出类图。
命令模式:将“请求”封装成对象,以便使用不同的请求、队列或日志来参数化其他对象。命令模式也支持撤销的操作。
3、完成多开关遥控器和撤销操作
假设遥控器现在有五个开关。我们已经有简单遥控器的经验,那么其他4个开关我们也将对应的命令对象设置上去就行了。定义两个数组用来记录开关对应的命令对象。
public class RemoteControl { Command[] onCommands; Command[] offCommands; public RemoteControl() { onCommands = new Command[5]; offCommands = new Command[5]; Command noCommand = new NoCommand(); for (int i = 0; i < 5; i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } } public void setCommand(int slot,Command commandOn, Command commandOff) { onCommands[slot] = commandOn; offCommands[slot] = commandOff; } //按下开关 public void OnButtonWasPressed(int slot) { onCommands[slot].execute(); } //关闭开关 public void OffButtonWasPressed(int slot) { offCommands[slot].execute(); } //打印出数组命令对象 public override string ToString() { var sb = new StringBuilder("\n------------Remote Control-----------\n"); for (int i = 0; i < onCommands.Length; i++) { sb.Append($"[slot{i}] {onCommands[i].GetType()}\t{offCommands[i].GetType()} \n"); } return sb.ToString(); } }
在遥控器中我们定义了一个Nocommand类,是为了对遥控器对应的开关初始化命令对象,避免为空报错或者消除开关调用命令对象时检查对象是否为空的判断。
public void OnButtonWasPressed(int slot) { if(onCommand[slot]!=null)) onCommands[slot].execute(); }
在许多设计模式中我们都能看到这种初始值或者空对象的使用。甚至有时候,空对象本身也被视为一种设计模式。(感觉这样代码比较优雅O(∩_∩)O)
遥控器完成了,我们还有做一项工作,就是撤销操作。
撤销操作我们同样在命令接口里面定义一个undo 方法。
public interface Command { //执行命令的方法 public void execute(); //撤销命令方法 public void undo(); }
然后我们让LightOnCommand实现undo方法,添加LightOffCommand命令对象。
public class LightOnCommand : Command { Light light; public LightOnCommand(Light light) { this.light = light; } public void execute() { light.on(); } public void undo() { light.off(); } } class LightOffCommand : Command { Light light; public LightOffCommand(Light light) { this.light = light; } public void execute() { light.off(); } public void undo() { light.on(); } }
遥控器里面添加撤销按钮操作UndoButtonWasPressed并用undoCommand属性存储上一次操作。
public class RemoteControl { Command[] onCommands; Command[] offCommands; Command undoCommand; public RemoteControl() { onCommands = new Command[5]; offCommands = new Command[5]; Command noCommand = new NoCommand(); for (int i = 0; i < 5; i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } } public void setCommand(int slot,Command commandOn, Command commandOff) { onCommands[slot] = commandOn; offCommands[slot] = commandOff; } //按下开关 public void OnButtonWasPressed(int slot) { onCommands[slot].execute(); undoCommand = onCommands[slot]; } //关闭开关 public void OffButtonWasPressed(int slot) { offCommands[slot].execute(); undoCommand = offCommands[slot]; } public void UndoButtonWasPressed() { undoCommand.undo(); } //打印出数组命令对象 public override string ToString() { var sb = new StringBuilder("\n------------Remote Control-----------\n"); for (int i = 0; i < onCommands.Length; i++) { sb.Append($"[slot{i}] {onCommands[i].GetType()}\t{offCommands[i].GetType()} \n"); } return sb.ToString(); } }
测试:
4、补充总结
补充:
①命令模式的接收者不一定要存在,之前提到过“聪明”和“傻瓜”命令对象,如果以“聪明”命令对象设计,调用者和接收者之间解耦程度比不上“傻瓜”命令对象,但是我们在使用比较简单的时候仍然可以使用“聪明”命令对象设计。
②撤销例子我们只做了返回最后一次操作,如果要撤销许多次我们可以对操作记录进行保存到堆栈,不管什么时候撤销,我们都可以从堆栈中取出最上层命令对象执行撤销操作。
命令模式常被用于队列请求,日志请求。当队列按照顺序取到存放的命令对象后调用执行方法就行了而不用去管具体执行什么。
日志请求在某些场合可以用来将所有动作记录在日志中,并能在系统死机后通过日志记录进行恢复到之前的状态(撤销)。对于更高级的的应用而言,这些技巧可以应用到事务(transaction)处理中。
通过简单到更进一步的实现讲解了命令模式和一些灵活点和需要注意的点,有什么理解不到位的欢迎指正。