本文的概念内容來自深入淺出設計模式一書.
項目需求

有這樣一個可程式設計的新型遙控器, 它有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下面是我的關于ASP.NET Core Web API相關技術的公衆号--草根專欄: