cover

你用過 Ctrl+Z 嗎?那你就用過 Command Pattern 的成果了。

每一個操作被包成一個 command 物件,執行的時候記錄下來,undo 的時候反向操作。沒有 Command Pattern,你怎麼知道要「撤銷什麼」?

先講結論

Command Pattern 把「一次操作」封裝成一個物件。好處是這個物件可以被:儲存、排隊、記錄歷史、撤銷重做。呼叫端不需要知道操作的細節,只要說「執行」就好。

classDiagram
    class Invoker {
        -history : Command[]
        +executeCommand(command) void
    }
    class Command {
        <<abstract>>
        +execute()* void
    }
    class SubmitRequestCommand {
        -receiver : WorkflowReceiver
        -requestId : String
        +execute() void
    }
    class ApproveRequestCommand {
        -receiver : WorkflowReceiver
        -requestId : String
        +execute() void
    }
    class WorkflowReceiver {
        +submit(requestId) void
        +approve(requestId) void
    }
    Invoker o-- Command : 執行與記錄
    Command <|-- SubmitRequestCommand
    Command <|-- ApproveRequestCommand
    SubmitRequestCommand --> WorkflowReceiver
    ApproveRequestCommand --> WorkflowReceiver

實戰:工作流程系統

一個請購單要經過「提交 → 審核 → 執行」。每一步都是一個 Command,Invoker 負責執行並記錄歷史。

Command 模式:命令佇列與執行歷史

class WorkflowReceiver {
    submit(requestId) { console.log(`Request ${requestId} submitted`); }
    approve(requestId) { console.log(`Request ${requestId} approved`); }
}
 
class SubmitRequestCommand {
    constructor(receiver, requestId) {
        this.receiver = receiver;
        this.requestId = requestId;
    }
    execute() { this.receiver.submit(this.requestId); }
}
 
class ApproveRequestCommand {
    constructor(receiver, requestId) {
        this.receiver = receiver;
        this.requestId = requestId;
    }
    execute() { this.receiver.approve(this.requestId); }
}
 
class Invoker {
    constructor() { this.history = []; }
 
    executeCommand(command) {
        command.execute();
        this.history.push(command); // 記錄下來,之後可以 undo
    }
}
 
const receiver = new WorkflowReceiver();
const invoker = new Invoker();
 
invoker.executeCommand(new SubmitRequestCommand(receiver, 'REQ-001'));
invoker.executeCommand(new ApproveRequestCommand(receiver, 'REQ-001'));
 
console.log(`已執行 ${invoker.history.length} 個命令`);

什麼場景最適合?

  • Undo/Redo:文字編輯器、繪圖工具(每個操作是一個 Command,undo 就是反向執行)
  • 排程系統:把 Command 放進 queue,按時間或順序執行
  • 巨集:錄製一連串操作,之後一鍵重播

要付出什麼代價?

每個操作都要寫一個 Command class。如果你的操作種類很多,class 數量會爆炸。這時候可以考慮用 function 取代 class(JavaScript 的函式本身就是 first-class object,天然適合做 Command)。


Command Pattern 就像你的瀏覽器歷史記錄——每個「上一頁」都是一次 undo。只是有些歷史記錄你不想被別人看到


延伸閱讀