Command 模式#
Command 是所有設計模式中最簡單、最優雅的一個。它的結構極為簡單——不過是一個只有一個方法的介面:

Figure 21.1: COMMAND pattern
public interface Command
{
void Execute();
}這個模式跨越了一條很有趣的界線——大多數類別將一組方法與一組對應的變數關聯在一起,但 Command 模式不這樣做。它封裝的是一個不帶任何變數的單一函式。從嚴格的物件導向觀點來看,這是在把函式提升到類別的層級,近乎功能分解(functional decomposition)。然而正是在這個邊界上,有趣的事情開始發生。
Simple Commands(簡單命令)#
作者曾為一家影印機製造商設計嵌入式即時軟體,團隊使用 Command 模式來控制硬體裝置,建立如 RelayOnCommand、MotorOffCommand、ClutchOnCommand 等類別階層:

Figure 21.2: Some simple commands for the copier software
- 對
RelayOnCommand呼叫Execute()就打開繼電器 - 對
MotorOffCommand呼叫Execute()就關閉馬達 - 馬達或繼電器的位址在建構時傳入
這個結構帶來巨大的好處——Sensor 完全不知道自己在做什麼。感測器偵測到事件時,只需呼叫綁定的 Command 的 Execute()。感測器不需要知道離合器、繼電器或紙張路徑的機械結構,其功能變得極為單純。
重點: 透過封裝「命令」的概念,Command 模式讓系統的邏輯互連與被連接的裝置徹底解耦。系統的「佈線」(wiring)可以完全在程式外部決定,甚至可以從設定檔讀取,無需重新編譯。
Transactions(交易)#
Command 模式的另一個常見用途是建立與執行交易(transactions)。以員工資料庫為例,使用者可以新增、刪除或修改員工。AddEmployeeTransaction 包含與 Employee 相同的資料欄位,加上指向 PayClassification 的指標:

Figure 21.5: AddEmployee transaction
Validate()方法:檢查所有資料的語法與語意正確性,甚至可以驗證資料與資料庫現有狀態的一致性Execute()方法:使用已驗證的資料更新資料庫
Physical and Temporal Decoupling(物理與時間解耦)#
Command 模式帶來兩種重要的解耦:
- 物理解耦(Physical Decoupling):將取得資料的程式碼(例如 GUI)與驗證和執行資料的程式碼分離。例如新增員工的資料可能來自對話框,但驗證與執行邏輯被分離到
AddEmployeeTransaction類別中 - 時間解耦(Temporal Decoupling):資料取得後,驗證和執行不必立即呼叫。交易物件可以放在清單中,稍後才驗證和執行
技巧: 假設資料庫只能在午夜到凌晨 1 點之間變更。使用者可以隨時輸入所有命令,驗證通過後,在午夜統一執行。Command 模式正好提供這種能力。
Undo 方法#
在 Command 介面加入 Undo() 方法,就能實作復原功能:

Figure 21.6: Undo variation of the COMMAND pattern
- 如果
Command衍生類別的Execute()方法能記住操作的細節,Undo()方法就能復原該操作並回到原始狀態 - 例如繪圖應用程式的
DrawCircleCommand:Execute()時記錄新圓的 ID,Undo()時根據 ID 刪除該圓 - 系統將已完成的命令推入堆疊,使用者按 Undo 時從堆疊彈出並呼叫
Undo()
Active Object 模式#
Active Object 是 Command 模式最有趣的用途之一。這是一種歷史悠久的多執行緒技術,用來提供簡單的多工核心。
核心概念非常簡單——ActiveObjectEngine 維護一個 Command 物件的連結串列。使用者可以新增命令或呼叫 Run()。Run() 方法會遍歷串列,逐一執行並移除每個命令:
public class ActiveObjectEngine
{
ArrayList itsCommands = new ArrayList();
public void AddCommand(Command c)
{
itsCommands.Add(c);
}
public void Run()
{
while (itsCommands.Count > 0)
{
Command c = (Command) itsCommands[0];
itsCommands.RemoveAt(0);
c.Execute();
}
}
}SleepCommand 範例#
關鍵的技巧在於:如果某個 Command 物件在執行時把自己重新放回串列,串列就永遠不會清空,Run() 就永遠不會結束。
SleepCommand 就是這樣的命令——它在指定的延遲時間過後執行一個 wakeup 命令:
public class SleepCommand : Command
{
private Command wakeupCommand = null;
private ActiveObjectEngine engine = null;
private long sleepTime = 0;
private DateTime startTime;
private bool started = false;
public void Execute()
{
DateTime currentTime = DateTime.Now;
if (!started)
{
started = true;
startTime = currentTime;
engine.AddCommand(this); // 重新放回引擎
}
else
{
TimeSpan elapsedTime = currentTime - startTime;
if (elapsedTime.TotalMilliseconds < sleepTime)
engine.AddCommand(this); // 時間未到,繼續等待
else
engine.AddCommand(wakeupCommand); // 時間到,執行 wakeup
}
}
}Run-to-Completion(RTC)執行緒#
這種程式與多執行緒程式有類似之處——每個 Command 實例都是一個 run-to-completion(RTC)任務,即每個 Command 實例必須執行完畢後,下一個才能執行。
- RTC 執行緒共享同一個執行期堆疊,不需要為每個執行緒分配獨立的堆疊
- 這在記憶體受限的系統中是一個強大的優勢
- 程式展現出不確定性行為(nondeterministic behavior),這是多執行緒系統的特徵
注意: 不確定性行為是多執行緒系統中痛苦和困難的根源。在嵌入式即時系統工作過的人都知道,偵錯非確定性行為非常困難。
結論#
- Command 模式的簡單性掩蓋了它的多用途性——可用於資料庫交易、裝置控制、多執行緒核心、GUI 復原/重做管理等
- 有人認為 Command 模式強調函式而非類別,打破了 OO 範式。這或許是真的,但在軟體開發的現實世界中,實用性勝過理論