當系統需要支援「撤銷/重做」、操作佇列、請求日誌、巨集記錄、交易回滾等功能時,將請求封裝為獨立物件是關鍵。命令模式將「請求的發起者」與「請求的執行者」解耦:請求被封裝為命令物件,持有接收者參考與執行參數,發起者只需呼叫 Execute(),無需知道具體執行細節。本文透過訊息系統的 Insert/Delete 操作,完整展示命令模式如何實現多級撤銷、重做與歷史記錄管理。
ICommand,定義 Execute()、UnExecute() 契約。SqlMessageCommand,持有 Action(Insert/Delete)與 MessageModel 參數,持有接收者 SqlMessage,實作執行與反向操作邏輯。SqlMessage,執行實際業務邏輯(Insert、Delete)。Message 類別,維護命令歷史清單 _listCommand 與當前指標 current,提供 Do()、Undo()、Redo() 高層操作。
public enum Action { Insert, Delete }
public interface ICommand
{
string Execute(); // 執行並回傳描述
string UnExecute(); // 撤銷並回傳描述
}
public class SqlMessage
{
public void Operation(Action action, MessageModel mm)
{
switch (action)
{
case Action.Insert: Insert(mm); break;
case Action.Delete: Delete(mm); break;
}
}
private void Insert(MessageModel mm) { /* 實際 SQL 插入 */ }
private void Delete(MessageModel mm) { /* 實際 SQL 刪除 */ }
}
接收者不包含命令管理邏輯,專注業務實作,可被多種命令複用。
public class SqlMessageCommand : ICommand
{
private Action _action;
private MessageModel _mm;
public SqlMessageCommand(Action action, MessageModel mm)
{
this._action = action;
this._mm = mm;
}
public string Execute()
{
new SqlMessage().Operation(_action, _mm);
return _action.ToString() + ":" + _mm.Message;
}
public string UnExecute()
{
_action = GetUndoAction(_action); // 計算反向動作
new SqlMessage().Operation(_action, _mm);
return _action.ToString() + ":" + _mm.Message;
}
private Action GetUndoAction(Action action)
{
return action == Action.Insert ? Action.Delete : Action.Insert;
}
}
關鍵設計:
Action 與 MessageModel。UnExecute() 透過 GetUndoAction() 計算反向操作(Insert↔Delete),實現語義級撤銷而非狀態快照恢復。Execute() 都建立新的 SqlMessage 接收者實例(無狀態),亦可注入共享實例。public class Message
{
private List<ICommand> _listCommand = new List<ICommand>();
private int current = 0; // 指向「下一個將執行的命令」索引
public string Do(Action action, MessageModel mm)
{
ICommand cmd = new SqlMessageCommand(action, mm);
string rtn = cmd.Execute();
// 關鍵:執行新命令時,截斷 current 之後的歷史(重做棧清空)
if (current < _listCommand.Count)
_listCommand.RemoveRange(current, _listCommand.Count - current);
_listCommand.Add(cmd);
current++;
return rtn;
}
public string Undo(int levels)
{
string rtn = "";
for (int i = 0; i < levels; i++)
{
if (current > 0)
{
ICommand cmd = _listCommand[--current];
rtn += cmd.UnExecute() + " ";
}
}
return rtn;
}
public string Redo(int levels)
{
string rtn = "";
for (int i = 0; i < levels; i++)
{
if (current < _listCommand.Count)
{
ICommand cmd = _listCommand[current++];
rtn += cmd.UnExecute() + " "; // 注意:此處呼叫 UnExecute 執行重做
}
}
return rtn;
}
}
核心機制解析:
_listCommand 記錄所有成功執行的命令歷史。current 指標分隔「已執行」與「可重做」區域:[0 ... current-1] = 已執行(可撤銷)[current ... Count-1] = 已撤銷(可重做)Do() 執行新命令時,移除 current 之後的舊重做歷史(分支覆蓋語義)。Undo() 將 current 向左移動,呼叫 UnExecute() 執行反向操作。Redo() 將 current 向右移動,同樣呼叫 UnExecute()(因反向的反向是正向)。
Message m = new Message();
// 執行 8 個操作
m.Do(Action.Insert, new MessageModel("第1條", Now));
m.Do(Action.Insert, new MessageModel("第2條", Now));
m.Do(Action.Insert, new MessageModel("第3條", Now));
m.Do(Action.Insert, new MessageModel("第4條", Now));
m.Do(Action.Delete, new MessageModel("第2條", Now));
m.Do(Action.Insert, new MessageModel("第5條", Now));
m.Do(Action.Delete, new MessageModel("第3條", Now));
m.Do(Action.Insert, new MessageModel("第6條", Now));
m.Do(Action.Insert, new MessageModel("第7條", Now));
// 撤銷 4 次:刪除第7、刪除第6、插入第3、刪除第5
m.Undo(4);
// 重做 2 次:插入第5、刪除第3
m.Redo(2);
// 再撤銷 3 次:插入第3、刪除第5、插入第2
m.Undo(3);
操作
Insert:第1條
Insert:第2條
Insert:第3條
Insert:第4條
Delete:第2條
Insert:第5條
Delete:第3條
Insert:第6條
Insert:第7條
撤銷4次操作
Delete:第7條 Delete:第6條 Insert:第3條 Delete:第5條
重復2次操作
Insert:第5條 Delete:第3條
撤銷3次操作
Insert:第3條 Delete:第5條 Insert:第2條
結果精確對應命令歷史棧操作,驗證撤銷/重做機制正確性。
實作 MacroCommand : ICommand 內含 List<ICommand>,Execute() 循環執行子命令,支援批次操作單次撤銷。
Invoker 內部使用 BlockingCollection<ICommand>,背景執行緒消費執行。接收者為狀態機時,命令可觸發狀態轉換,撤銷需恢復狀態(可結合備忘錄模式)。
命令模式將「請求」物件化,賦予請求一等公民地位:可傳遞、儲存、撤銷、重做、組合、記錄。本範例以訊息系統 Insert/Delete 為例,完整實現了命令封裝、接收者分離、請求者歷史管理(雙棧指標法)與多級撤銷重做的核心機制。實務中廣泛應用於:編輯器撤銷/重做、IDE 重構操作、資料庫遷移腳本、工作流引擎、遊戲指令重放、分散式系統命令模式(CQRS/Event Sourcing)。掌握命令模式,能讓你在需要「操作可逆、可記錄、可佇列、可遠端」的系統設計中,構建出靈活強大的指令處理架構。