Command 模式#

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

Figure 21.1: COMMAND pattern

public interface Command
{
  void Execute();
}

這個模式跨越了一條很有趣的界線——大多數類別將一組方法與一組對應的變數關聯在一起,但 Command 模式不這樣做。它封裝的是一個不帶任何變數的單一函式。從嚴格的物件導向觀點來看,這是在把函式提升到類別的層級,近乎功能分解(functional decomposition)。然而正是在這個邊界上,有趣的事情開始發生。

Simple Commands(簡單命令)#

作者曾為一家影印機製造商設計嵌入式即時軟體,團隊使用 Command 模式來控制硬體裝置,建立如 RelayOnCommandMotorOffCommandClutchOnCommand 等類別階層:

Figure 21.2: Some simple commands for the copier software

  • RelayOnCommand 呼叫 Execute() 就打開繼電器
  • MotorOffCommand 呼叫 Execute() 就關閉馬達
  • 馬達或繼電器的位址在建構時傳入

這個結構帶來巨大的好處——Sensor 完全不知道自己在做什麼。感測器偵測到事件時,只需呼叫綁定的 CommandExecute()。感測器不需要知道離合器、繼電器或紙張路徑的機械結構,其功能變得極為單純。

重點: 透過封裝「命令」的概念,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() 方法就能復原該操作並回到原始狀態
  • 例如繪圖應用程式的 DrawCircleCommandExecute() 時記錄新圓的 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 範式。這或許是真的,但在軟體開發的現實世界中,實用性勝過理論