本文的概念内容来自深入浅出设计模式一书.
有这样一个可编程的新型遥控器, 它有7个可编程插槽, 每个插槽可连接不同的家用电器设备. 每个插槽对应两个按钮: 开, 关(ON, OFF). 此外还有一个全局的取消按钮(UNDO).
现在客户想使用这个遥控器来控制不同厂家的家用电器, 例如电灯, 热水器, 风扇, 音响等等.
客户提出让我编写一个接口, 可以让这个遥控器控制插在插槽上的一个或一组设备.
看一下目前各家厂商都有哪些家用电器??:
问题来了, 这些家用电器并没有共同的标准....几乎各自都有自己的一套控制方法.. 而且以后还要添加很多种家用电器.
那就需要考虑一下设计方案了:
首先要考虑分离关注点(Separation of concerns), 遥控器应该可以解释按钮动作并可以发送请求, 但是它不应该了解家用电器和如何开关家用电器等.
但是目前遥控器只能做开关功能, 那么怎么让它去控制电灯或者音响呢? 我们不想让遥控器知道这些具体的家用电器, 更不想写出下面的代码:
if slot1 == Light then Light.On()
else if slot1 == Hub....
说到这就不得不提到命令模式(Command Pattern)了.
命令模式允许你把动作的请求者和动作的实际执行者解耦. 这里, 动作的请求者就是遥控器, 而执行动作的对象就是某个家用电器.
这是怎么解耦的呢? 怎么可能实现呢?
这就需要引进"命令对象(command object)"了. 命令对象会封装在某个对象上(例如卧室的灯)执行某个动作的请求(例如开灯). 所以, 如果我们为每一个按钮都准备一个命令对象, 那么当按钮被按下的时候, 我们就会调用这个命令对象去执行某些动作. 遥控器本身并不知道具体执行的动作是什么, 它只是有一个命令对象, 这个命令对象知道去对哪些电器去做什么样的操作. 就这样, 遥控器和电灯解耦了.
一个快餐厅:
客户给服务员订单, 服务员把订单放到柜台并说: "有新订单了", 然后厨师按照订单准备饭菜.
让我们仔细分析一下它们是怎么交互的:
客户来了, 说我想要汉堡, 奶酪....就是创建了一个订单 (createOrder()).
订单上面写着客户想要的饭菜.
服务员取得订单 takeOrder(), 把订单拿到柜台喊道: "有新订单了" (调用orderUp())
厨师按照订单的指示把饭菜做好 (orderUp()里面的动作).
分析一下这个例子的角色和职责:
回到我们的需求, 参考快餐店的例子, 使用命令模式做一下设计:
客户Client创建了一个命令(Command)对象. 相当于客人拿起了一个订单(点菜)准备开始点菜, 我在琢磨遥控器的槽需要插哪些家用电器. 命令对象和接收者是绑定在一起的. 相当于菜单和厨师, 遥控器的插槽和目标家用电器.
命令对象只有一个方法execute(), 里面封装了调用接收者实际控制操作的动作. 相当于饭店订单的orderUp().
客户调用setCommand()方法. 相当于客户想好点什么菜了, 就写在订单上面了. 我也想好遥控器要控制哪些家电了, 列好清单了.
调用者拿着已经setCommand的命令对象, 在未来某个时间点调用命令对象上面的execute()方法. 相当于服务员拿起订单走到柜台前, 大喊一声: "有订单来了, 开始做菜吧". 相当于我把遥控器和设备的接口连接上了, 准备开始控制.
最后接收者执行动作. 相当于厨师做饭. 家用电器使用自己独有的控制方法进行动作.
这里面:
客户 --- 饭店客人, 我
命令 --- 订单, 插槽
调用者 --- 服务员, 遥控器
setCommand()设置命令 --- takeOrder() 取订单, 插上需要控制的电器
execute() 执行 --- orderUp() 告诉柜台做饭, 按按钮
接收者 --- 厨师, 家电
所有命令对象需要实现的接口:
namespace CommandPattern.Abstractions { public interface ICommand { void Execute(); } }
一盏灯:
using System; namespace CommandPattern.Devices { public class Light { public void On() { Console.WriteLine("Light is on"); } public void Off() { Console.WriteLine("Light is off"); } } }
控制灯打开的命令:
using CommandPattern.Abstractions; using CommandPattern.Devices; namespace CommandPattern.Commands { public class LightOnCommand : ICommand { private readonly Light light; public LightOnCommand(Light light) { this.light = light; } public void Execute() { this.light.On(); } } }
车库门:
using System; namespace CommandPattern.Devices { public class GarageDoor { public void Up() { Console.WriteLine("GarageDoor is opened."); } public void Down() { Console.WriteLine("GarageDoor is closed."); } } }
收起车库门命令:
using CommandPattern.Abstractions; using CommandPattern.Devices; namespace CommandPattern.Commands { public class GarageDoorOpen : ICommand { private readonly GarageDoor garageDoor; public GarageDoorOpen(GarageDoor garageDoor) { this.garageDoor = garageDoor; } public void Execute() { garageDoor.Up(); } } }
简易的遥控器:
using CommandPattern.Abstractions; namespace CommandPattern.RemoteControls { public class SimpleRemoteControl { public ICommand Slot { get; set; } public void ButtonWasPressed() { Slot.Execute(); } } }
运行测试:
using System; using CommandPattern.Commands; using CommandPattern.Devices; using CommandPattern.RemoteControls; namespace CommandPattern { class Program { static void Main(string[] args) { var remote = new SimpleRemoteControl(); var light = new Light(); var lightOn = new LightOnCommand(light); remote.Slot = lightOn; remote.ButtonWasPressed(); var garageDoor = new GarageDoor(); var garageDoorOpen = new GarageDoorOpenCommand(garageDoor); remote.Slot = garageDoorOpen; remote.ButtonWasPressed(); } } }
命令模式把请求封装成一个对象, 从而可以使用不同的请求对其它对象进行参数化, 对请求排队, 记录请求的历史, 并支持取消操作.
类图:
效果图:
遥控器:
using System.Text; using CommandPattern.Abstractions; using CommandPattern.Commands; namespace CommandPattern.RemoteControls { public class RemoteControl { private ICommand[] onCommands; private ICommand[] offCommands; public RemoteControl() { onCommands = new ICommand[7]; offCommands = new ICommand[7]; var noCommand = new NoCommand(); for (int i = 0; i < 7; i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } } public void SetCommand(int slot, ICommand onCommand, ICommand offCommand) { onCommands[slot] = onCommand; offCommands[slot] = offCommand; } 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, 它是一个空的类, 只是为了初始化command 以便以后不用判断是否为null.
关灯:
using CommandPattern.Abstractions; using CommandPattern.Devices; namespace CommandPattern.Commands { public class LightOffCommand: ICommand { private readonly Light light; public LightOffCommand(Light light) { this.light = light; } public void Execute() { light.Off(); } } }
下面试一个有点挑战性的, 音响:
namespace CommandPattern.Devices { public class Stereo { public void On() { System.Console.WriteLine("Stereo is on."); } public void Off() { System.Console.WriteLine("Stereo is off."); } public void SetCD() { System.Console.WriteLine("Stereo is set for CD input."); } public void SetVolume(int volume) { System.Console.WriteLine($"Stereo‘s volume is set to {volume}"); } } }
音响打开命令:
using CommandPattern.Abstractions; namespace CommandPattern.Devices { public class StereoOnWithCDCommand : ICommand { private readonly Stereo stereo; public StereoOnWithCDCommand(Stereo stereo) { this.stereo = stereo; } public void Execute() { stereo.On(); stereo.SetCD(); stereo.SetVolume(10); } } }
测试运行:
using System; using CommandPattern.Commands; using CommandPattern.Devices; using CommandPattern.RemoteControls; namespace CommandPattern { class Program { static void Main(string[] args) { var remote = new RemoteControl(); var light = new Light(); var lightOn = new LightOnCommand(light); var lightOff = new LightOffCommand(light); var garageDoor = new GarageDoor(); var garageDoorOpen = new GarageDoorOpenCommand(garageDoor); var garageDoorClose = new GarageDoorCloseCommand(garageDoor); var stereo = new Stereo(); var stereoOnWithCD = new StereoOnWithCDCommand(stereo); var stereoOff = new StereoOffCommand(stereo); remote.SetCommand(0, lightOn, lightOff); remote.SetCommand(1, garageDoorOpen, garageDoorClose); remote.SetCommand(2, stereoOnWithCD, stereoOff); System.Console.WriteLine(remote); remote.OnButtonWasPressed(0); remote.OffButtonWasPressed(0); remote.OnButtonWasPressed(1); remote.OffButtonWasPressed(1); remote.OnButtonWasPressed(2); remote.OffButtonWasPressed(2); } } }
该需求的设计图:
还有一个问题...取消按钮呢?
1. 可以在ICommand接口里面添加一个undo()方法, 然后在里面执行上一次动作相反的动作即可:
namespace CommandPattern.Abstractions { public interface ICommand { void Execute(); void Undo(); } }
例如开灯:
using CommandPattern.Abstractions; using CommandPattern.Devices; namespace CommandPattern.Commands { public class LightOnCommand : ICommand { private readonly Light light; public LightOnCommand(Light light) { this.light = light; } public void Execute() { light.On(); } public void Undo() { light.Off(); } } }
遥控器:
using System.Text; using CommandPattern.Abstractions; using CommandPattern.Commands; namespace CommandPattern.RemoteControls { public class RemoteControlWithUndo { private ICommand[] onCommands; private ICommand[] offCommands; private ICommand undoCommand; public RemoteControlWithUndo() { onCommands = new ICommand[7]; offCommands = new ICommand[7]; var noCommand = new NoCommand(); for (int i = 0; i < 7; i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } undoCommand = noCommand; } public void SetCommand(int slot, ICommand onCommand, ICommand offCommand) { onCommands[slot] = onCommand; offCommands[slot] = offCommand; } 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(); } } }
测试一下:
using System; using CommandPattern.Commands; using CommandPattern.Devices; using CommandPattern.RemoteControls; namespace CommandPattern { class Program { static void Main(string[] args) { var remote = new RemoteControl(); var light = new Light(); var lightOn = new LightOnCommand(light); var lightOff = new LightOffCommand(light); var stereo = new Stereo(); var stereoOnWithCD = new StereoOnWithCDCommand(stereo); var stereoOff = new StereoOffCommand(stereo); remote.SetCommand(0, lightOn, lightOff); remote.SetCommand(1, stereoOnWithCD, stereoOff); System.Console.WriteLine(remote); remote.OnButtonWasPressed(0); remote.OffButtonWasPressed(0); remote.OnButtonWasPressed(1); remote.OffButtonWasPressed(1); } } }
基本是OK的, 但是有点小问题, 音响的开关状态倒是取消了, 但是它的音量(也包括播放介质, 不过这个我就不去实现了)并没有恢复.
下面就来处理一下这个问题.
修改Stereo:
namespace CommandPattern.Devices { public class Stereo { public Stereo() { Volume = 5; } public void On() { System.Console.WriteLine("Stereo is on."); } public void Off() { System.Console.WriteLine("Stereo is off."); } public void SetCD() { System.Console.WriteLine("Stereo is set for CD input."); } private int volume; public int Volume { get { return volume; } set { volume = value; System.Console.WriteLine($"Stereo‘s volume is set to {volume}"); } } } }
命令:
using CommandPattern.Abstractions; namespace CommandPattern.Devices { public class StereoOnWithCDCommand : ICommand { private int previousVolume; private readonly Stereo stereo; public StereoOnWithCDCommand(Stereo stereo) { this.stereo = stereo;
previousVolume = stereo.Volume; } public void Execute() { stereo.On(); stereo.SetCD(); stereo.Volume = 10; } public void Undo() { stereo.Volume = previousVolume; stereo.SetCD(); stereo.Off(); } } }
运行:
Party Mode (聚会模式):
思路是创建一种命令, 它可以执行多个其它命令
MacroCommand:
using CommandPattern.Abstractions; namespace CommandPattern.Commands { public class MacroCommand : ICommand { private ICommand[] commands; public MacroCommand(ICommand[] commands) { this.commands = commands; } public void Execute() { for (int i = 0; i < commands.Length; i++) { commands[i].Execute(); } } public void Undo() { for (int i = 0; i < commands.Length; i++) { commands[i].Undo(); } } } }
使用这个MacroCommand:
using System; using CommandPattern.Abstractions; using CommandPattern.Commands; using CommandPattern.Devices; using CommandPattern.RemoteControls; namespace CommandPattern { class Program { static void Main(string[] args) { var light = new Light(); var lightOn = new LightOnCommand(light); var lightOff = new LightOffCommand(light); var garageDoor = new GarageDoor(); var garageDoorOpen = new GarageDoorOpenCommand(garageDoor); var garageDoorClose = new GarageDoorCloseCommand(garageDoor); var stereo = new Stereo(); var stereoOnWithCD = new StereoOnWithCDCommand(stereo); var stereoOff = new StereoOffCommand(stereo); var macroOnCommand = new MacroCommand(new ICommand[] { lightOn, garageDoorOpen, stereoOnWithCD }); var macroOffCommand = new MacroCommand(new ICommand[] { lightOff, garageDoorClose, stereoOff }); var remote = new RemoteControl(); remote.SetCommand(0, macroOnCommand, macroOffCommand); System.Console.WriteLine(remote); System.Console.WriteLine("--- Pushing Macro on ---"); remote.OnButtonWasPressed(0); System.Console.WriteLine("--- Pushing Macro off ---"); remote.OffButtonWasPressed(0); } } }
这个工作队列是这样工作的: 你添加命令到队列的结尾, 在队列的另一端有几个线程. 线程这样工作: 它们从队列移除一个命令, 调用它的execute()方法, 然后等待调用结束, 然后丢弃这个命令再获取一个新的命令.
这样我们就可以把计算量限制到固定的线程数上面了. 工作队列和做工作的对象也是解耦的.
这个例子就是使用命令模式记录请求动作的历史, 如果出问题了, 可以按照这个历史进行恢复.
这个系列的代码我放在这里了: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp
使用 C# (.NET Core) 实现命令设计模式 (Command Pattern)
原文:https://www.cnblogs.com/cgzl/p/8830596.html