本文的概念內容來自深入淺出設計模式一書.
專案需求
有這樣一個可程式設計的新型遙控器, 它有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()裡面的動作).
分析一下這個例子的角色和職責:
- 訂單裡封裝了做飯菜的請求. 可以把訂單想象成一個物件, 這個物件就像是對做飯這個動作的請求. 並且它可以來回傳遞. 訂單實現了一個只有orderUp()方法的介面, 這個方法裡面封裝了做飯的操作流程. 訂單同時對動作實施者的引用(廚師). 因為都封裝了, 所以服務員不知道訂單裡面有啥也不知道廚師是誰. 服務員只傳遞訂單, 並呼叫orderUp().
- 所以, 服務員的工作就是傳遞訂單並且呼叫orderUp(). 服務員的取訂單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