iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

Codetopia 新手日記:設計模式與原則的 30 天學習之旅系列 第 17

Day 17:Command(命令模式):將「派工」本身化為可撤銷、可排程的「王牌命令」!

  • 分享至 

  • xImage
  •  

Codetopia 創城記 (17)|Command(命令模式):將「派工」本身化為可撤銷、可排程的「王牌命令」!

1) 今日熱點 (故事開場 & 痛點) ⚡️

Codetopia 的市政調度中心,氣氛比昨晚還緊張。

「總設計師,我們有麻煩了!」派工調度員 Liam(Dispatcher)的聲音從通訊器傳來,背景音滿是此起彼落的電話鈴聲。

昨夜,Tess 與 Kaito 才剛用責任鏈模式(Chain of Responsibility)漂亮地將棘手陳情案分流完畢,讓權責歸屬變得清晰。但,真正的風暴現在才開始。

「Rhea!Rhea!聽到請回答!」Liam 對著麥克風喊道。

「在路上!怎麼了?」道路維修隊領班 Rhea(Crew Lead)的聲音有些雜訊。

「計畫變更!剛剛都更署下了臨時路權變更,你們得改道去 B 區!不對...等等,市長室說要優先處理海港音樂祭的場佈......」

Rhea 的小隊剛出發,命令就得改;派出去的工程車,可能下一秒就得撤回;更別提那些必須在清晨六點準時、自動執行的批次任務。如果每個動作都是一個寫死的 API 呼叫,光是處理「撤銷」、「重做」跟「排程」這三件鳥事,就足以讓整個調度系統徹底崩潰。

(旁白:「是的,你沒看錯。當流程的執行方式本身變得複雜時,問題就不再是『誰來做』,而是『怎麼記下這堆該死的指令』。」)

看來,我們需要的不只是一個分流系統,而是一個能將「動作」本身打包、管理、甚至可以反悔的機制。

2) 術語卡 💡

  • GoF|Command:將一個「請求」或「操作」封裝成一個獨立的物件。這讓你可以參數化客戶端(Client),將請求排入佇列、記錄請求日誌,以及實現可撤銷的操作。

  • EIP|Command Bus / Work Queue:一種訊息傳遞模式,命令被發送到一個中央通道(Bus)或佇列(Queue),由一個或多個工作者(Worker)非同步地拉取並執行。

  • MAS|任務代理 + DF:在多代理系統中,具備不同能力的代理人(Agent)會在黃頁(Directory Facilitator)上註冊。調度中心只需將任務(命令)發布,黃頁會媒合給能執行該任務的代理。

3) 笑中帶淚 (反例/壞味道) ⚠️

在引入 Command 模式之前,調度中心的早期原型系統是怎麼處理這一切的呢?讓我們把時間倒帶,看看工程師 Andy 寫下的第一版災難性程式碼。

他把所有的派工邏輯,全都硬生生地塞在 UI 按鈕的點擊事件裡。

# 反例:沒有 Command,直接在服務層寫死邏輯
def on_dispatch_clicked(case, need_revoke, schedule_time):
    # 直接呼叫 API,一個動作一槍
    api.send_car("crew-a", case.loc)
    api.notify_residents(case.block)

    # 想要撤銷?行,寫一組「反向操作」的 if
    if need_revoke:
        print("🚨 夭壽!派錯了,趕快摳回來!")
        api.recall_car("crew-a")
        api.revert_notify(case.block)

    # 需要排程?更精彩了,手動拼接 crontab 字串...
    if schedule_time == "06:00":
        print("⏰ 早上六點的鬧鐘,我手動設定...")
        cron_job_string = f"0 6 * * * api.send_car crew-a {case.loc}"
        # ...然後把這串字手動貼到某個神秘的排程腳本裡
        # (Andy 祈禱自己沒有拼錯字)

這段程式碼簡直是災難現場。每次新增一種派工、一種撤銷邏輯,Liam 的操作介面後方就長出一片新的 if/else 熱帶雨林。至於排程?那更是土法煉鋼,充滿了拼錯字的風險與半夜被 call 爆的惡夢。(旁白:「真正的問題是責任錯置:排程的責任應該交給 Scheduler 或 Bus,且直接內插字串更有被注入攻擊的風險!」)

4) 王牌出手 (核心觀念) ⚙️

「夠了!」總設計師終於看不下去。「我們不是在寫腳本,我們在建城市!我們需要的是命令模式(Command Pattern)!」

一句話解釋:把每一個「派工動作」都變成一個標準化的「命令物件(Command Object)」。

這個物件包含了執行該動作所需的一切資訊。調度員 Liam(Invoker)的工作不再是親自打電話或呼叫 API,而是「提交」這些命令物件。這些命令被交給命令台或佇列,由它們在適當的時機執行。而前線的 Rhea 和她的維修隊(Receiver)則專注於如何完成命令中交辦的任務。

重要語意區分:對於已成功執行的命令,我們用 Undo(撤銷);對於尚未執行的排程命令,我們應該用 Cancel(取消)。Undo 的前提是命令已被記錄於歷史中。

這樣做的好處是:

  1. 解耦:提交命令的人(Liam)和執行命令的人(Rhea)徹底分開。

  2. 可管理:命令物件可以被儲存、排隊、傳遞。

  3. 可擴展:要新增功能?寫一個新的命令類別就好,不動舊程式。

何時用 (When to Use)

  • 需要撤銷/重做 (Undo/Redo):當每個命令物件都帶有 undo() 方法時,實現撤銷就易如反掌。

  • 需要排程/佇列 (Scheduling/Queueing):命令物件可以被放入佇列,等待稍後執行或依序執行。

  • 需要巨集命令 (Macro Commands):將一連串的命令組合成一個單一的複合命令,一鍵執行。

  • 需要日誌與審計 (Logging & Auditing):可以輕鬆記錄所有被執行的命令,方便追蹤、除錯,甚至災難重演(Replay)。

何時不要用 (When NOT to Use)

  • 操作極簡:如果你的應用程式只有幾個簡單、不需要回溯或排程的動作,引入 Command 模式反而會過度設計。

  • 固定流程骨架:如果流程的骨架是固定的,只是其中幾個步驟的具體實現不同,那可能更適合策略模式(Strategy)或樣板方法模式(Template Method)。

5) 導播切景 (表格+兩張 Mermaid) 🎥

導播,鏡頭拉一下!讓我們從三個不同尺度,看看這個「王牌命令」在 Codetopia 是如何運作的。

視角 觀念/模式 在 Codetopia 的說法
微觀 (GoF) Command 派工單物件:每個動作(出隊、封路)都是可執行、可撤銷的標準工單。
中觀 (EIP/EDA) Command Bus / Work Queue 派工命令總線:Liam 將工單丟進佇列,由 Worker 非同步拉取執行,失敗則進入「死信區」。
宏觀 (MAS) 任務代理 + DF 黃頁派工:Liam 只需下達任務,黃頁系統會自動媒合給具備相應能力的現場代理(道路、照明、警導)。

5.1 微觀 (GoF|UML 類別圖)

這張圖展示了命令模式的核心結構:Invoker(調度台)持有並觸發 Command,而具體的 Command 實現(如 DispatchCrewCmd)則呼叫 Receiver(現場作業系統 FieldOps)來完成工作。History 則負責記錄,以支援撤銷與重做。

https://ithelp.ithome.com.tw/upload/images/20251002/20178500NoQk86IEN5.png

5.2 中觀 (EIP/EDA|時序圖)

當中觀尺度看,Worker 扮演「命令執行器」的角色,負責從佇列拉取命令並觸發。真正的 Receiver 則是後端的 FieldOpsService。只有當命令執行成功後,才會被寫入 History,此刻起才支援 Undo

https://ithelp.ithome.com.tw/upload/images/20251002/20178500XMVb0IEZAJ.png

6) 最小實作 (Pseudo / Python 風) 💻

Talk is cheap, show me the code! 讓我們看看用 Python 風格的偽代碼,如何實現這個優雅且工程正確的系統。

# 現場作業系統 (Receiver)
class FieldOps:
    def send_car(self, crew, loc): print(f"✅ 派出 {crew} 前往 {loc}...")
    def recall_car(self, crew): print(f"↩️ 撤回 {crew}...")
    def block_road(self, loc): print(f"🚧 封鎖 {loc} 道路...")
    def unblock_road(self, loc): print(f"✅ 解除 {loc} 道路封鎖...")
    def notify(self, block): print(f"📢 通知 {block} 區居民...")
    def send_correction(self, block): print(f"📢 發送更正通知給 {block} 區居民...")

# 命令介面
class Command:
    def execute(self): raise NotImplementedError
    def undo(self): raise NotImplementedError

# --- 具體命令 ---

class DispatchCrewCmd(Command):
    def __init__(self, crew, loc, ops: FieldOps):
        self.crew, self.loc, self.ops = crew, loc, ops
    def execute(self): self.ops.send_car(self.crew, self.loc)
    def undo(self): self.ops.recall_car(self.crew)

class BlockRoadCmd(Command):
    def __init__(self, loc, ops: FieldOps):
        self.loc, self.ops = loc, ops
    def execute(self): self.ops.block_road(self.loc)
    def undo(self): self.ops.unblock_road(self.loc)

# 註記:通知類命令無法真正「撤銷」,只能用「補償」(compensate)
class NotifyResidentsCmd(Command):
    def __init__(self, block, ops: FieldOps):
        self.block, self.ops = block, ops
    def execute(self): self.ops.notify(self.block)
    def undo(self): self.ops.send_correction(self.block)

# 巨集命令:一次執行多個命令

class MacroCommandError(Exception):
    """巨集命令執行失敗後的錯誤型別,攜帶回滾資訊"""
    def __init__(self, original: Exception, partial):
        super().__init__(str(original))
        self.original = original
        self.partial = list(partial)  # 已成功且已嘗試回滾的子命令
        self.rolled_back = True

class MacroCmd(Command):
    def __init__(self, commands):
        self.commands = commands
    def execute(self):
        completed_commands = []
        try:
            for cmd in self.commands:
                cmd.execute()
                completed_commands.append(cmd)
        except Exception as e:
            print(f"巨集命令執行失敗: {e},開始局部回滾...")
            for cmd in reversed(completed_commands):
                try:
                    cmd.undo()
                except Exception as undo_e:
                    # 如果連 undo 都失敗,必須記錄下來,避免卡死
                    print(f"🚨 緊急!命令 {cmd.__class__.__name__} 的 undo 操作失敗: {undo_e}")
            # 以自訂錯誤型別回報,攜帶回滾與部分進度資訊
            raise MacroCommandError(e, completed_commands)
    def undo(self):
        for cmd in reversed(self.commands):
            cmd.undo()

# 命令調度台 (Invoker) 與【修正後】的歷史紀錄
class History:
    def __init__(self):
        self.undo_stack, self.redo_stack = [], []
    def push_after_execute(self, cmd):
        self.undo_stack.append(cmd)
        self.redo_stack.clear()
    def move_undo_to_redo(self, cmd):
        self.redo_stack.append(cmd)
    def move_redo_to_undo(self, cmd):
        self.undo_stack.append(cmd)

class Invoker:
    def __init__(self):
        self.history = History()
        self.current_cmd = None
    def set_command(self, cmd: Command):
        self.current_cmd = cmd
    # 註解:此為本地同步模式示範。若經由 Bus/Queue,
    # 應在 Worker 成功回報時,才由對應的 Handler 呼叫 history.push_after_execute()
    def execute_command(self):
        if self.current_cmd:
            self.current_cmd.execute()
            self.history.push_after_execute(self.current_cmd)
    def undo(self):
        cmd = self.history.undo_stack.pop() if self.history.undo_stack else None
        if cmd:
            cmd.undo()
            self.history.move_undo_to_redo(cmd)
    def redo(self):
        cmd = self.history.redo_stack.pop() if self.history.redo_stack else None
        if cmd:
            cmd.execute()
            self.history.move_redo_to_undo(cmd)

7) 鄉民出題 (動手+反模式紅旗) ❓

  1. 情境題:回頭看 3) 笑中帶淚 的那段 if/else 熱帶雨林程式碼,如果當時總設計師指派你(而不是 Andy)來重構,你會如何建立你的第一個 Command 類別?它會是什麼?需要哪些參數?

  2. 實作題:請你動手為 MacroCmd 加上「日誌記錄」功能。在 executeundo 時,都能印出是哪個子命令被執行或撤銷了。

  3. 反模式紅旗 (Red Flags)

    • 把撤銷寫成反向 API:當你發現系統裡有大量的 send_car()recall_car() 這種成對出現的 API,卻沒有統一的 undo() 介面時,就是警訊。

    • Invoker 與 Receiver 強耦合:如果你的 Invoker 直接 new 一個 FieldOps 物件來用,那你就失去了佇列、排程、遠端執行的所有彈性。

    • 巨集命令缺乏回滾策略:一個沒有 try-catch處理 undo 失敗機制的 MacroCmd,就像一串鞭炮,一點燃就停不下來,中間炸膛了也沒人管。

8) 城市望遠鏡 (進階視野) 🔭

今天我們把「動作」物件化了。在更宏大的架構中,這個觀念會演化:

  • EIP 層面:命令會被塑造成標準化的可持久訊息,包含 {id, type, payload, runAt, correlationId, idempotencyKey} 等欄位。其中,idempotencyKey (例如由 commandTypepayload 的 hash 組成) 對於防止重複執行至關重要。流經 Command Bus,支援延遲、重試,失敗則轉入死信佇列(DLQ)

  • Actor Model 層面:每個 Worker 都是一個獨立的 Actor,從自己的信箱(Mailbox)中拉取命令來執行。這天然地隔離了錯誤,並允許多個命令並行處理,互不干擾。

  • MAS 層面:命令不再只是資料,而是帶有「意圖」的溝通語言(ACL)。Liam 只需宣告「我需要一個具備道路維修能力的代理來執行此任務」,DF 就會找到 Rhea 的代理,並將命令派發過去。審計記錄更可用於長期的系統行為重放與分析。

9) 結語 & 預告

動作物件化,可撤銷可排程;命令進佇列,巨集可回放。

今天,我們給了 Liam 一個強大的武器,讓混亂的調度工作變得井然有序。但新的問題來了:當數百個工單命令在系統中流轉時,Liam 該如何有效率地逐一巡檢所有「未完工」的工單,而不用管它們是單一命令還是巨集命令呢?

明日預告:Day 18|Iterator(逐站巡覽)—— 無論容器結構多複雜,我都能給你一個統一的巡覽介面!

9) 結語 & 預告

動作物件化,可撤銷可排程;命令進佇列,巨集可回放。

今天,我們給了 Liam 一個強大的武器,讓混亂的調度工作變得井然有序。但新的問題來了:當數百個工單命令在系統中流轉時,Liam 該如何有效率地逐一巡檢所有「未完工」的工單,而不用管它們是單一命令還是巨集命令呢?

明日預告:Day 18|Iterator(逐站巡覽)—— 無論容器結構多複雜,我都能給你一個統一的巡覽介面!


10) 附錄:ASCII 版圖示

為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:


10.1 微觀視角:GoF 命令模式類別圖 (ASCII)

┌──────────────────────────────────────────────────────────────────────┐
│                         Command 模式結構圖                              │
└──────────────────────────────────────────────────────────────────────┘

                    ┌─────────────────────┐
                    │   <<interface>>     │
                    │      Command        │
                    ├─────────────────────┤
                    │ + execute()         │
                    │ + undo()            │
                    └──────────┬──────────┘
                               │
                               │ implements
        ┌──────────────────────┼──────────────────────┐
        │                      │                      │
┌───────▼────────┐   ┌─────────▼────────┐   ┌────────▼───────────┐
│ DispatchCrewCmd│   │  BlockRoadCmd    │   │NotifyResidentsCmd  │
├────────────────┤   ├──────────────────┤   ├────────────────────┤
│ - crew         │   │ - loc            │   │ - block            │
│ - loc          │   │ - ops: FieldOps  │   │ - ops: FieldOps    │
│ - ops:FieldOps │   ├──────────────────┤   ├────────────────────┤
├────────────────┤   │ + execute()      │   │ + execute()        │
│ + execute()    │   │ + undo()         │   │ + undo()           │
│ + undo()       │   └──────────────────┘   └────────────────────┘
└────────┬───────┘
         │                  ┌─────────────────────┐
         │                  │     MacroCmd        │
         │                  ├─────────────────────┤
         │                  │ - commands: []      │
         │                  ├─────────────────────┤
         │                  │ + execute()         │
         │                  │ + undo()            │
         │                  └──────┬──────────────┘
         │                         │
         │                         │ aggregates many
         └─────────────────────────┘ Command objects

         ┌──────────calls──────────┐
         │                         │
         ▼                         ▼
┌────────────────┐        ┌────────────────┐
│    Invoker     │        │    FieldOps    │
├────────────────┤        │   (Receiver)   │
│ - history      │        ├────────────────┤
├────────────────┤        │ + send_car()   │
│ + submit(cmd)  │        │ + recall_car() │
│ + undo()       │        │ + block_road() │
│ + redo()       │        │ + unblock_rd() │
└───────┬────────┘        │ + notify()     │
        │                 │ + send_corr()  │
        │ uses            └────────────────┘
        ▼
┌────────────────┐
│    History     │
├────────────────┤
│ - undo_stack   │
│ - redo_stack   │
├────────────────┤
│ + push()       │
│ + move_undo()  │
│ + move_redo()  │
└────────────────┘

圖示說明

  • Command 介面定義統一的 execute()undo() 操作
  • 具體命令類別實現介面,封裝對 FieldOps 的呼叫
  • MacroCmd 聚合多個命令,形成複合命令
  • Invoker 透過 History 管理命令的執行與撤銷
  • FieldOps 作為真正的接收者,執行實際操作

10.2 中觀視角:EIP 命令總線時序圖 (ASCII)

┌──────────────────────────────────────────────────────────────────────┐
│                    命令總線執行流程時序圖                               │
└──────────────────────────────────────────────────────────────────────┘

 Liam        調度控制台      命令總線/佇列      Worker       現場作業服務
(調度員)      (Invoker)        (Bus)        (執行器)       (Receiver)
   │              │               │              │              │
   │ 提交巨集命令   │               │              │              │
   │ (06:00執行)   │               │              │              │
   ├─────────────>│               │              │              │
   │              │  enqueue(cmd, │              │              │
   │              │  runAt="06:00")              │              │
   │              ├──────────────>│              │              │
   │              │               │              │              │
   │              │               │   ⏰ 等待至   │              │
   │              │               │    06:00     │              │
   │              │               │      ...     │              │
   │              │               │              │              │
   │              │               │ deliver(cmd) │              │
   │              │               ├─────────────>│              │
   │              │               │              │              │
   │              │               │              │ execute(步驟1)│
   │              │               │              ├─────────────>│
   │              │               │              │              │
   │              │               │              │ execute(步驟2)│
   │              │               │              ├─────────────>│
   │              │               │              │              │
   │              │               │              │ execute(步驟3)│
   │              │               │              ├─────────────>│
   │              │               │              │              │
   │              │               │              │ ack(全部成功) │
   │              │               │              │<─────────────┤
   │              │               │              │              │
   │              │  回報結果      │              │              │
   │              │  (含審計ID)    │              │              │
   │              │<──────────────┼──────────────┤              │
   │              │               │              │              │
   │              │ history.      │              │              │
   │              │ push_after_   │              │              │
   │              │ execute(cmd)  │              │              │
   │              │──┐            │              │              │
   │              │  │            │              │              │
   │              │<─┘            │              │              │
   │              │               │              │              │
   │   ✅ 完成    │               │              │              │
   │<─────────────┤               │              │              │
   │              │               │              │              │

【關鍵點】
 ① 命令提交後進入佇列等待
 ② Worker 在指定時間拉取並執行
 ③ 成功後才寫入 History,此時才支援 Undo
 ④ 審計 ID 用於追蹤與重放

流程說明

  1. 提交階段:Liam 透過調度控制台提交命令,指定執行時間
  2. 佇列等待:命令在總線中等待,直到指定時間到達
  3. 非同步執行:Worker 拉取命令並逐步執行所有子操作
  4. 確認記錄:執行成功後回報結果,並寫入歷史記錄
  5. 支援撤銷:只有成功執行並記錄的命令才能被撤銷

10.3 命令執行與撤銷流程圖 (ASCII)

┌──────────────────────────────────────────────────────────────────────┐
│                      命令生命週期狀態機                                 │
└──────────────────────────────────────────────────────────────────────┘

                    ┌──────────────┐
                    │  命令建立     │
                    │  (Created)   │
                    └──────┬───────┘
                           │
                           │ submit()
                           ▼
                    ┌──────────────┐
                    │  排隊等待     │
                    │  (Queued)    │
                    └──────┬───────┘
                           │
                           │ Worker pulls
                           ▼
                    ┌──────────────┐
                    │  執行中       │
                    │ (Executing)  │
                    └──────┬───────┘
                           │
               ┌───────────┴───────────┐
               │                       │
          ✅ 成功                    ❌ 失敗
               │                       │
               ▼                       ▼
        ┌──────────────┐        ┌──────────────┐
        │  已完成       │        │  失敗        │
        │ (Completed)  │        │  (Failed)    │
        └──────┬───────┘        └──────┬───────┘
               │                       │
               │ push to History       │ 進入 DLQ
               ▼                       │ 或重試
        ┌──────────────┐               │
        │  可撤銷       │               ▼
        │ (Undoable)   │        ┌──────────────┐
        └──────┬───────┘        │  等待重試     │
               │                │ (Retry Queue)│
               │ undo()         └──────────────┘
               ▼
        ┌──────────────┐
        │  已撤銷       │
        │  (Undone)    │
        └──────┬───────┘
               │
               │ redo()
               ▼
        ┌──────────────┐
        │  重新執行     │
        │ (Re-executed)│
        └──────────────┘


┌──────────────────────────────────────────────────────────────────────┐
│                    Undo/Redo 堆疊運作示意                              │
└──────────────────────────────────────────────────────────────────────┘

初始狀態:
Undo Stack: [Cmd1, Cmd2, Cmd3]  ← 最近執行的命令在頂端
Redo Stack: []

執行 undo() 後:
Undo Stack: [Cmd1, Cmd2]        ← Cmd3 被 pop 出來
Redo Stack: [Cmd3]              ← Cmd3 推入 Redo Stack

再執行一次 undo() 後:
Undo Stack: [Cmd1]
Redo Stack: [Cmd3, Cmd2]        ← Cmd2 也推入

執行 redo() 後:
Undo Stack: [Cmd1, Cmd2]        ← Cmd2 重新執行並推回
Redo Stack: [Cmd3]

❗ 若此時執行新命令 Cmd4:
Undo Stack: [Cmd1, Cmd2, Cmd4]  ← 新命令加入
Redo Stack: []                  ← Redo Stack 被清空!
                                  (這是標準的編輯器行為)

狀態說明

  • Created → Queued:命令被提交到佇列
  • Queued → Executing:Worker 開始執行命令
  • Executing → Completed:執行成功,寫入歷史
  • Executing → Failed:執行失敗,進入重試或死信佇列
  • Completed → Undoable:成功的命令可以被撤銷
  • Undoable ↔ Undone:可以在撤銷和重做之間切換

10.4 巨集命令錯誤處理流程 (ASCII)

┌──────────────────────────────────────────────────────────────────────┐
│                  MacroCmd 錯誤回滾機制                                 │
└──────────────────────────────────────────────────────────────────────┘

MacroCmd 包含:[Cmd1, Cmd2, Cmd3, Cmd4, Cmd5]

執行流程:
  ┌────┐   ┌────┐   ┌────┐   ┌────┐   ┌────┐
  │Cmd1│──>│Cmd2│──>│Cmd3│──>│Cmd4│──>│Cmd5│
  └─┬──┘   └─┬──┘   └─┬──┘   └─┬──┘   └────┘
    │        │        │        │
    ✅       ✅       ✅       ❌ 失敗!
    │        │        │
    ▼        ▼        ▼
  已完成    已完成    已完成    ← completed_commands = [Cmd1, Cmd2, Cmd3]


❌ 檢測到錯誤,開始回滾:

  回滾順序:Cmd3 → Cmd2 → Cmd1 (reversed)

  ┌────────────────────────────────────────┐
  │  try:                                  │
  │    Cmd3.undo()  ✅ 成功                │
  │    Cmd2.undo()  ✅ 成功                │
  │    Cmd1.undo()  ✅ 成功                │
  │                                        │
  │  結果:全部回滾成功                      │
  │  狀態:系統恢復到執行前                  │
  └────────────────────────────────────────┘

🚨 如果 undo 也失敗:

  ┌────────────────────────────────────────┐
  │  try:                                  │
  │    Cmd3.undo()  ✅ 成功                │
  │    Cmd2.undo()  ❌ undo 失敗!          │
  │    │                                   │
  │    └─> 記錄錯誤日誌                     │
  │    └─> 發送告警通知                     │
  │    └─> 繼續嘗試 Cmd1.undo()            │
  │                                        │
  │  結果:部分回滾失敗                      │
  │  動作:需人工介入處理                    │
  │  記錄:MacroCommandError 攜帶詳細資訊   │
  └────────────────────────────────────────┘


【最佳實踐】
✓ 每個 Cmd 的 undo() 必須實作錯誤處理
✓ MacroCmd 應記錄每個子命令的執行狀態
✓ 提供 compensate() 方法處理無法撤銷的操作
✓ 將失敗資訊寫入審計日誌供事後分析

上一篇
Day 16:Chain of Responsibility(責任鏈模式):棘手陳情案的逐級節點
系列文
Codetopia 新手日記:設計模式與原則的 30 天學習之旅17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言