第 9 / 11 部 待錄

行為型模式II-觀察者與命令

《Design Patterns 設計模式(GoF)》Podcast 準備稿:行為型模式 II — Observer、Command 與協作型模式

書名: 設計模式 Design Patterns(Elements of Reusable Object-Oriented Software) 作者: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides(Gang of Four) 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|Design Patterns.發布訂閱與把動作物件化:Observer/Command (9/11) 涵蓋範圍: Observer、Command(深講)+ Chain of Responsibility、Mediator(速覽)

背景速覽

行為型模式的第二棒,主軸是「物件之間怎麼解耦地溝通」。這集深講兩個現代軟體的超級主角:Observer(觀察者)——也就是大名鼎鼎的「發布—訂閱(publish-subscribe)」,幾乎撐起整個事件驅動和響應式程式設計;以及 Command(命令)——把「動作本身變成物件」,從而拿到 Undo、佇列、日誌等一票超能力。

最後速覽兩個「協作型」模式:**Chain of Responsibility(責任鏈)**和 Mediator(中介者),它們和 Observer 一起回答「一群物件怎麼協作而不互相焊死」。這集講完,你會發現現代前後端的事件系統,幾乎都是這幾個模式的後代。

一句話重點

Observer 定義「一對多」的依賴——主題一變,所有訂閱者自動收到通知,發布者不需要知道訂閱者是誰;Command 把「請求」封裝成物件,於是動作可以被傳遞、排隊、記錄、復原;兩者加上 Chain of Responsibility 與 Mediator,共同解決「物件如何鬆耦合地協作」這個行為型核心難題。

值得討論的重點

1. Observer:發布—訂閱,現代軟體的神經系統

Intent:定義物件之間的一對多依賴,當一個物件(Subject)狀態改變,所有依賴它的物件(Observer)自動收到通知並更新。別名 DependentsPublish-Subscribe

動機:試算表和長條圖顯示同一份資料,但彼此不認識。改試算表,長條圖立刻更新。解法是 Subject 維護一份 Observer 名單,狀態變了就廣播通知所有 Observer——它不需要知道 Observer 是誰,只認抽象介面。

兩大優點:

  • 抽象耦合:Subject 只認 Observer 的抽象介面,不知具體類別;兩者還能分屬不同抽象層(低層 Subject 通知高層 Observer,維持分層)。
  • 支援廣播:通知不指定接收者,Observer 隨時可增減,Subject 不受影響。

但缺點很真實,務必講:

  • 意外的連鎖更新:Observer 彼此不知道對方存在,一個小操作可能引爆 Observer 及其依賴物件的連鎖更新,難追蹤、容易多餘更新。
  • Update 協定太簡單:通知裡常常沒講「什麼變了」,Observer 可能得花大力氣推導差異。

兩個關鍵設計決策:

  • Push vs Pull 模型:Push(Subject 推詳細變更,Observer 可能收到不需要的)vs Pull(Observer 主動查,Subject 保持無知,但可能較慢)。這是個重要取捨。
  • ChangeManager:依賴關係很複雜時,引入一個集中管理者來協調映射與更新——而這個 ChangeManager 本身就是一個 Mediator,通常還是 Singleton。(看,模式又組合起來了。)

2. Command:把「動作」變成一等公民物件

Intent:把請求封裝成物件,使你能用不同請求參數化 client、把請求排隊或記錄日誌、並支援可復原(undo) 操作。別名 Action、Transaction

動機(呼應 EP2):UI 工具包的按鈕、選單要回應操作,但工具包不知道具體該做什麼——那取決於應用程式。解法:定義抽象 CommandExecute),ConcreteCommand 存一個 receiver、實作 Execute 去呼叫 receiver 的操作。MenuItem 持有一個 Command,點擊就 Execute

威力來自「動作物件化」後解鎖的一切:

  • 解耦 Invoker 和 Receiver:發請求的(按鈕)完全不需知道誰、怎麼執行。同一個 Command 能被選單和按鈕共用。
  • Command 是一等物件:能被傳遞、儲存、操作。
  • Undo/Redo:加 Unexecute,維護命令歷史就能任意層級復原。
  • MacroCommand:用 Composite 串多個命令成複合命令(注意 undo 要反序執行子命令的 unexecute)。
  • 容易新增命令:不改現有類別,符合開閉原則。

要提一個進階點:undo/redo 多次後可能有誤差累積(hysteresis),狀態偏離原值——可搭配 Memento(EP10)儲存更完整狀態緩解。

3. 協作型雙速覽:Chain of Responsibility 與 Mediator

這兩個和 Observer 同屬「一群對等物件怎麼協作」,速覽帶過、給直覺即可:

  • Chain of Responsibility(責任鏈):把請求沿一條物件鏈傳遞,每個物件「能處理就處理、不能就轉給下一個」。發請求的不知道誰會處理(implicit receiver)。經典例子:GUI 的 context help(按鈕 → 對話框 → 應用程式,誰能答誰答)。現代影子:Web 框架的 middleware 管線(Express、ASP.NET 的 pipeline)、例外處理鏈、事件冒泡,全是責任鏈。
  • Mediator(中介者):引入一個中介物件,封裝一群物件的互動,用「一對多」取代「多對多」。經典例子:對話框裡一堆 widget 互相影響(選了列表項就更新輸入框、輸入框空了就停用按鈕),全交給一個 FontDialogDirector 協調,widget 之間彼此不認識。代價:中介者容易膨脹成難維護的「上帝物件」。現代影子:MVC 的 Controller、聊天室伺服器、前端的狀態管理中樞。

對照記憶:Observer 是「廣播」、Chain 是「接力」、Mediator 是「總機」——三種解耦協作的不同拓撲。

注意事項

⚠️ Observer 的『意外連鎖更新』是真實世界的大坑,務必提醒。 一個天真的 setter 觸發通知,通知又觸發別的更新,更新又觸發通知……輕則重複計算、效能爛,重則無窮迴圈或難以重現的 bug。響應式框架(RxJS、Vue 的響應式)花大量工夫處理這個問題(批次更新、依賴追蹤、防止循環)。所以 Observer 看似優雅,但**「誰在什麼時候被通知、會不會連鎖」必須想清楚**,否則它會反咬你。

⚠️ Mediator 很容易養出一隻『上帝物件』。 它把分散的耦合「集中」成自己的複雜度——這在 colleague 少時很清爽,但 colleague 一多、互動一複雜,中介者就膨脹成幾千行、什麼都管的怪物。Mediator 不是免費把複雜度消滅,只是搬家。用它的時候要盯著:這個中介者是不是又胖又雜了?該拆了嗎?

專家補充

💡 Observer 是『事件驅動』與『響應式程式設計』的祖先,現代軟體幾乎離不開它。 前端的 addEventListener、Node 的 EventEmitter、Redux/Vuex 的 store 訂閱、RxJS 的 Observable、訊息佇列的 pub/sub、資料庫的 trigger/CDC——全是 Observer 的後代。GoF 在 1994 年描述的「一對多、廣播、訂閱者隨時增減」,今天是整個事件驅動架構的地基。這集很適合讓聽眾意識到:他每天寫的監聽器,都是這個模式。

💡 Command 是『把行為當資料』的物件導向版,這個思想威力驚人。 一旦動作變成物件(資料),它就能被序列化、傳到別台機器、存進佇列、寫進日誌、重播。Event Sourcing、CQRS、訊息佇列的 job、前端 Redux 的 action、資料庫的 WAL(write-ahead log)、git 的 commit——本質都是 Command 思想:記錄「做了什麼動作」而非只記錄「結果」,於是你能 undo、能重播、能審計。這是 GoF 裡少數「越到現代越重要」的模式。

💡 責任鏈 = middleware,這個對照能讓後端工程師秒懂。 每個 Express/Koa/ASP.NET 的 middleware 就是責任鏈上的一個節點:拿到請求,能處理就處理、要嘛加工後 next() 傳給下一個。GoF 的「沿鏈傳遞、誰能處理誰處理」在 1994 年是 GUI help 系統,今天是每個 Web 框架的請求管線核心。

討論問題

🎙️ 開場鉤子:「button.addEventListener('click', ...)——你寫過一萬次。那一行,就是 Observer 模式。今天我們講兩個現代軟體離不開的模式:一個讓物件能『訂閱』彼此的變化(Observer),一個把『動作』本身變成可以儲存、重播、復原的物件(Command)。」

🎙️ 自問自答:「Observer、Chain、Mediator 都在解耦協作,怎麼分?」——拓撲不同。Observer 是廣播(一變全收到)、Chain 是接力(傳到有人處理)、Mediator 是總機(全部透過中央協調)。三種星形/鏈形/中心化的不同連法。

🎙️ 帶走的一題:「想想你系統裡的 Undo、或事件重播、或操作日誌——它們背後幾乎都是 Command:把『做了什麼』當成資料記下來。下次設計一個『需要可復原 / 可審計』的功能時,先問:我能不能把這個動作變成一個物件?」

更大範圍關聯

  • Observer 與事件驅動/響應式架構:是 pub/sub、EventEmitter、RxJS、Redux 訂閱的共同祖先,撐起整個事件驅動生態(見 [軟體工程領域切分藍圖] 的前端與後端事件支脈)。
  • Command 與 Event Sourcing / CQRS:把行為當資料的思想延伸到 WAL、訊息佇列、git commit、Redux action——是「記錄動作而非結果」這條架構主線的源頭。
  • Chain of Responsibility 與 middleware:Web 框架的請求管線、例外處理鏈、事件冒泡都是它的現代化身。
  • Mediator 與 MVC/狀態管理:MVC 的 Controller、前端狀態中樞都是 Mediator;它與 Facade 的差異(雙向 vs 單向協定)值得對照(呼應 EP5)。

錄製建議

  • 建議時長:約 24-26 分鐘。配比:Observer 約 9 分(含 push/pull、連鎖更新坑、現代響應式)、Command 約 9 分(含 undo 與「行為當資料」的延伸)、Chain + Mediator 速覽約 5 分(含 middleware 對照)。
  • 討論策略:兩個主角都用「你每天在寫的東西」開場(addEventListener、Undo)瞬間接地。Observer 務必講「連鎖更新」這個真實大坑,避免只報喜。Command 的高潮是「把行為當資料 → Event Sourcing/Redux/git」這條現代延伸,這是讓聽眾驚艷的點。協作型三模式用「廣播/接力/總機」一句話收,幫聽眾建立辨識。
  • 提醒:這集是 11 個行為模式裡「最貼近現代日常」的一集,多用當代類比。