前言:命令模式我們平常可能會經常使用,如果我們不了解命令模式的結構和定義那么在使用的時候也不會將它對號入座。
舉個例子:在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)處理中。
通過簡單到更進一步的實現講解了命令模式和一些靈活點和需要注意的點,有什么理解不到位的歡迎指正。
