Command Pattern(命令模式)
是什麼?
Command Pattern 將請求(Request)封裝成物件,讓你可以把請求存起來、傳遞、排隊、記錄,甚至做 Undo/Redo。命令物件包含了執行操作所需的所有資訊。
ℹ️GoF 分類
Command 屬於行為型模式(Behavioral Pattern),重點在於將「發出請求」和「執行請求」解耦。
什麼時候用?
用以下 if/then 條件判斷:
- 如果你需要 Undo/Redo 功能 → 用 Command,每個操作封裝為物件並記錄歷史
- 如果操作需要排隊、延遲執行或批次處理 → 用 Command 放入 Queue
- 如果你需要操作的完整稽核紀錄(Audit Log,指記錄誰在什麼時間做了什麼操作的日誌)→ 用 Command 序列化存檔
- 如果一組操作必須全部成功或全部回滾(Transaction 語意)→ 用 Command + Memento
- 如果操作很簡單且不需要撤銷或排隊 → 直接呼叫方法就好
什麼時候不該用?
⚠️過度設計警告
如果操作很簡單且不需要撤銷或排隊,直接呼叫方法就好。每個操作都包一層 Command 會讓程式碼膨脹,只有在需要「把操作當作資料來處理」時才值得。
執行流程
定義 Command 介面
宣告 Execute 和可選的 Undo 方法
實作具體 Command
每個命令封裝接收者和操作參數
建立 Invoker
持有命令參考,負責觸發執行
建立 Receiver
實際執行業務邏輯的物件
Client 組裝
建立命令、設定接收者、交給 Invoker
流程解讀:Command 的四個角色各司其職。Command 介面定義「做什麼」的合約(Execute/Undo)。ConcreteCommand 封裝了 Receiver(誰來做)和參數(用什麼資料做)。Invoker 不關心命令內容,只負責在正確時機觸發。Receiver 是真正執行業務邏輯的物件。這種分離讓「發出請求」和「執行請求」完全解耦。
程式碼範例
C# 版本
// 1. Command 介面
public interface ICommand
{
void Execute();
void Undo();
}
// 2. Receiver:文字編輯器
public class TextEditor
{
public string Content { get; private set; } = "";
public void InsertText(string text) => Content += text;
public void DeleteLast(int count) =>
Content = Content[..^Math.Min(count, Content.Length)];
public override string ToString() => $"「{Content}」";
}
// 3. 具體 Command
public class InsertTextCommand : ICommand
{
private readonly TextEditor _editor;
private readonly string _text;
public InsertTextCommand(TextEditor editor, string text)
{
_editor = editor;
_text = text;
}
public void Execute() => _editor.InsertText(_text);
public void Undo() => _editor.DeleteLast(_text.Length);
}
// 4. Invoker:命令歷史管理器
public class CommandHistory
{
private readonly Stack<ICommand> _history = new();
public void ExecuteCommand(ICommand command)
{
command.Execute();
_history.Push(command);
}
public void UndoLast()
{
if (_history.Count > 0)
_history.Pop().Undo();
}
}
// 5. 使用
var editor = new TextEditor();
var history = new CommandHistory();
history.ExecuteCommand(new InsertTextCommand(editor, "Hello"));
history.ExecuteCommand(new InsertTextCommand(editor, " World"));
Console.WriteLine(editor); // 「Hello World」
history.UndoLast();
Console.WriteLine(editor); // 「Hello」
history.UndoLast();
Console.WriteLine(editor); // 「」TypeScript 版本
// 1. Command 介面
interface Command {
execute(): void;
undo(): void;
}
// 2. Receiver
class TextEditor {
content = "";
insertText(text: string) { this.content += text; }
deleteLast(count: number) {
this.content = this.content.slice(0, -count || undefined) ?? "";
}
toString() { return `「${this.content}」`; }
}
// 3. 具體 Command
class InsertTextCommand implements Command {
constructor(private editor: TextEditor, private text: string) {}
execute() { this.editor.insertText(this.text); }
undo() { this.editor.deleteLast(this.text.length); }
}
// 4. Invoker
class CommandHistory {
private history: Command[] = [];
executeCommand(command: Command) {
command.execute();
this.history.push(command);
}
undoLast() {
const command = this.history.pop();
command?.undo();
}
}
// 5. 使用
const editor = new TextEditor();
const history = new CommandHistory();
history.executeCommand(new InsertTextCommand(editor, "Hello"));
history.executeCommand(new InsertTextCommand(editor, " World"));
console.log(editor.toString()); // 「Hello World」
history.undoLast();
console.log(editor.toString()); // 「Hello」Python 版本
from abc import ABC, abstractmethod
# 1. Command 介面
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
# 2. Receiver
class TextEditor:
def __init__(self):
self.content = ""
def insert_text(self, text: str) -> None:
self.content += text
def delete_last(self, count: int) -> None:
self.content = self.content[:-count] if count else self.content
def __str__(self) -> str:
return f"「{self.content}」"
# 3. 具體 Command
class InsertTextCommand(Command):
def __init__(self, editor: TextEditor, text: str):
self._editor = editor
self._text = text
def execute(self) -> None:
self._editor.insert_text(self._text)
def undo(self) -> None:
self._editor.delete_last(len(self._text))
# 4. Invoker
class CommandHistory:
def __init__(self):
self._history: list[Command] = []
def execute_command(self, command: Command) -> None:
command.execute()
self._history.append(command)
def undo_last(self) -> None:
if self._history:
self._history.pop().undo()
# 5. 使用
editor = TextEditor()
history = CommandHistory()
history.execute_command(InsertTextCommand(editor, "Hello"))
history.execute_command(InsertTextCommand(editor, " World"))
print(editor) # 「Hello World」
history.undo_last()
print(editor) # 「Hello」Java 版本
import java.util.Stack;
// 1. Command 介面
public interface Command {
void execute();
void undo();
}
// 2. Receiver
public class TextEditor {
private StringBuilder content = new StringBuilder();
public void insertText(String text) { content.append(text); }
public void deleteLast(int count) {
int start = Math.max(0, content.length() - count);
content.delete(start, content.length());
}
public String toString() { return "「" + content + "」"; }
}
// 3. 具體 Command
public class InsertTextCommand implements Command {
private final TextEditor editor;
private final String text;
public InsertTextCommand(TextEditor editor, String text) {
this.editor = editor;
this.text = text;
}
public void execute() { editor.insertText(text); }
public void undo() { editor.deleteLast(text.length()); }
}
// 4. Invoker
public class CommandHistory {
private final Stack<Command> history = new Stack<>();
public void executeCommand(Command cmd) {
cmd.execute();
history.push(cmd);
}
public void undoLast() {
if (!history.isEmpty()) history.pop().undo();
}
}
// 5. 使用
TextEditor editor = new TextEditor();
CommandHistory history = new CommandHistory();
history.executeCommand(new InsertTextCommand(editor, "Hello"));
history.executeCommand(new InsertTextCommand(editor, " World"));
System.out.println(editor); // 「Hello World」
history.undoLast();
System.out.println(editor); // 「Hello」結構圖
結構解讀:Client 把 ConcreteCommand 交給 Invoker,Invoker 在適當時機呼叫 Execute()。ConcreteCommand 內部持有 Receiver 的參考,Execute() 實際上是呼叫 Receiver 的方法。Invoker 完全不知道 Receiver 的存在,只認識 Command Interface。這讓同一個 Invoker 可以執行任何類型的命令。
實戰補充
💡資深開發者經驗
CQRS 架構:Command Query Responsibility Segregation 把 Command(寫入)和 Query(讀取)分開處理。MediatR 套件在 .NET 生態系中廣泛用來實作 Command 分發。
Command Queue:把命令放入 Message Queue(如 RabbitMQ、Azure Service Bus)可以實現非同步處理和流量削峰。
Macro Command:把多個 Command 組合成一個 Macro,一次執行或一次撤銷。這是 Composite Pattern + Command Pattern 的經典組合。
序列化命令:Command 物件可以序列化存到資料庫,實現 Event Sourcing——系統的所有狀態都是一連串命令的結果。
理解測驗
🤔 Command Pattern 的核心目的是什麼?
🤔 在 Command Pattern 中,Invoker 的職責是什麼?
🤔 以下哪個場景最不適合用 Command Pattern?
面試常見問題
Q: Command Pattern 的 Undo 和 Memento Pattern 的 Undo 有什麼差別?何時用哪個?
A: Command Undo 透過「反向操作」回復(如 Insert 的 Undo 是 Delete)。Memento Undo 透過「狀態快照」回復(恢復到之前保存的完整狀態)。選擇標準:如果每個操作都有明確的反向操作 → Command Undo 更省記憶體;如果操作複雜到難以定義反向操作 → Memento 更安全。實務中常搭配使用。
Q: CQRS 和 Command Pattern 是什麼關係?
A: CQRS(Command Query Responsibility Segregation)將「寫入操作(Command)」和「讀取操作(Query)」分開處理。Command Pattern 提供了封裝寫入操作的機制。在 .NET 中,MediatR 套件的 IRequest + IRequestHandler 就是 Command Pattern 在 CQRS 中的實踐。
相關模式
| 模式 | 關係 | |------|------| | Memento | 搭配使用 — Command 記錄「做了什麼」,Memento 記錄「之前的狀態」,兩者結合實現完整的 Undo 系統 | | Composite | Macro Command 就是 Composite + Command — 把多個 Command 組合成一個,一次執行或撤銷 | | Strategy | 都是把行為封裝成物件,但 Command 強調「請求的物件化」(可排隊、撤銷),Strategy 強調「演算法的替換」 | | Chain of Responsibility | 可搭配使用 — 命令沿著責任鏈傳遞,找到合適的處理者來執行 |
重點整理
💡一句話記住
Command Pattern = 點菜單:把請求寫在紙上,可以排隊、取消、重做。 口訣:「請求變物件,排撤記三合一」
| 概念 | 說明 | |------|------| | Command(命令介面) | 定義 Execute 和 Undo 方法 | | ConcreteCommand(具體命令) | 封裝 Receiver 和操作參數 | | Invoker(呼叫者) | 持有命令並觸發執行 | | Receiver(接收者) | 實際執行業務邏輯的物件 | | 核心好處 | 解耦請求發送者與執行者,支援 Undo/Redo | | 代價 | 每個操作都需要一個命令類別,類別數量會增加 |