第 2 / 11 部 待錄

案例研究-文件編輯器Lexi

《Design Patterns 設計模式(GoF)》Podcast 準備稿:案例研究 — 文件編輯器 Lexi

書名: 設計模式 Design Patterns(Elements of Reusable Object-Oriented Software) 作者: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides(Gang of Four) 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|Design Patterns.一個文件編輯器,藏了八個模式 (2/11) 涵蓋範圍: 第2章 案例研究 — 設計一個 WYSIWYG 文件編輯器 Lexi(七大設計問題、八個模式)

背景速覽

第一集講了抽象的原則,這集 GoF 立刻給你一個真實的案例讓原則落地。他們設計一個叫 Lexi 的所見即所得(WYSIWYG)文件編輯器——有編輯區、選單、捲軸、可以混排文字和圖形。然後從這個編輯器裡,抽出七個真實的設計問題,每個問題剛好導向一到兩個設計模式。

這集是整季的「為什麼模式不是憑空冒出來的」證明集。模式不是學者在象牙塔裡發明的抽象,而是一群人為了解決「怎麼讓文件結構統一處理、怎麼換排版演算法、怎麼支援跨平台」這些具體痛點,反覆磨出來的解法。講完這集,聽眾對後面 23 個模式會有「啊,原來是要解決這種問題」的直覺。

一句話重點

模式不是抽象概念,而是問題驅動的產物——Lexi 這一個編輯器就用上了八個模式,而它們反覆指向同一條原則:封裝那個會變化的概念(結構會變、演算法會變、平台會變、操作會變)。

值得討論的重點

1. 文件結構 → Composite:讓「一個字」和「一整頁」長一樣

第一個問題:文件由字元、圖形、行、欄、頁組成,層層巢狀。挑戰是——能不能統一處理單一元素(一個字)和群組元素(一整欄)?

GoF 的解法是 Recursive Composition(遞迴組合):定義一個 Glyph 抽象基底類別,無論是 Character、Row 還是 Column 都是 Glyph,都會 Draw、都回報 Bounds、都能管理子元素。於是「字組成行、行組成欄、欄組成頁」變成一棵樹,client 不用區分「這是葉子還是容器」。這就是 Composite 模式的雛形。

這裡先埋一個鉤子:聽眾日後寫任何「樹狀/巢狀結構」——檔案系統、UI 元件樹、組織圖——背後都是這招。

2. 格式化 → Strategy:排版演算法可以抽換

第二個問題:「文件結構」和「怎麼排版」是兩件事。同樣一棵 Glyph 樹,可以用「快但醜」的簡單演算法排,也可以用 TeX 那種「慢但漂亮」的全域最佳化排。

解法是把排版演算法抽出來,封裝成 Compositor 物件(SimpleCompositorTeXCompositor),讓 Composition 持有一個 Compositor。要換演算法,換物件就好,甚至能 run-time 動態切換。這就是 Strategy 模式——封裝「會變的演算法」。注意這裡又是「組合勝過繼承」的實踐:不是繼承出一個 TeXComposition,而是抱一個 TeXCompositor 進來。

3. UI 裝飾 → Decorator:別讓子類別爆炸

第三個問題:編輯區要加邊框、要加捲軸,而且可能隨時增減。如果用繼承做,你會得到 BorderedCompositionScrollableCompositionBorderedScrollableComposition……組合爆炸。

解法是 Transparent Enclosure(透明封裝):定義 MonoGlyph,它包住一個 Glyph、預設把所有請求轉發進去;子類別 BorderScroller 在轉發前後加自己的動作(先讓內容畫、再畫邊框)。因為 MonoGlyph 也是 Glyph,client 分不出來,於是裝飾可以任意堆疊、任意排序。這就是 Decorator 模式——run-time 動態加職責,不靠繼承。

4. 跨平台外觀 → Abstract Factory;跨視窗系統 → Bridge(兩個常被搞混的問題)

這兩個問題很值得對照講,因為它們長得像但本質不同:

  • 多種外觀風格(Motif、Mac…)→ Abstract Factory:如果到處 new MotifScrollBar(),換風格就要改一堆地方、還可能漏改(Mac 應用裡冒出 Motif 選單)。解法是 GUIFactory 抽象工廠,CreateScrollBarCreateButton,具體工廠(MotifFactory…)回傳整套對應風格的 widget。重點是「產品家族」——一次換掉一整組,保證風格一致。
  • 多種視窗系統(X、PM、Mac)→ Bridge:這裡你無法自己實作視窗系統,必須橋接到既有平台 API。解法是把 Window(面向應用開發者的邏輯抽象)和 WindowImp(封裝平台實作)拆成兩個獨立階層,Window 把請求委託給 WindowImp。Bridge 解決的是「抽象和實作兩條軸各自會變」的乘法爆炸。

對照點:Abstract Factory 是「我要建出一整家族相容的物件」,Bridge 是「我要讓抽象層和實作層各自獨立演化」。而且 GoF 還示範了它們可以搭配——用 Abstract Factory 來建正確的 WindowImp。

5. 使用者操作 → Command:把「動作」變成物件,順便拿到 Undo

第五個問題:使用者操作(存檔、剪貼、改字型)散在各處,又可能從選單、按鈕、快捷鍵觸發。解法是把每個請求包成 Command 物件Execute),MenuItem 持有一個 Command,點擊就 Execute

精彩的是 Undo/Redo 幾乎是「免費附贈」:在介面加 Unexecute,再維護一個 Command History(命令列表+「現在」指標),向左 Undo、向右 Redo,就能支援任意層級的復原。這集可以強調:把動作物件化,是一個威力極大的觀念——一旦動作是物件,它就能被儲存、排隊、記錄、復原。

6. 拼字檢查與斷字 → Iterator + Visitor:遍歷和分析要分家

最後一個問題最有深度,拆成兩半:

  • 怎麼存取散在不同資料結構裡的文字 → Iterator:把遍歷邏輯從 Glyph 抽出來,封裝成 Iterator(ArrayIteratorPreorderIterator…)。新增遍歷方式只要加 Iterator 子類別,不動 Glyph。
  • 怎麼分析而不每次改 GlyphVisitor:拼字檢查、斷字、字數統計……都是新的「操作」。如果每加一個分析就改所有 Glyph 類別,會很痛。解法是 Visitor:定義 SpellingCheckingVisitorHyphenationVisitor,Glyph 提供 Accept(Visitor),透過雙重分派呼叫對應方法。新增分析=新增 Visitor,不動 Glyph。

但要誠實講出 Visitor 的代價(後面 EP11 詳談):它假設 Glyph 結構穩定。如果你常新增 Glyph 子類別,那所有 Visitor 都得跟著改——這時 Visitor 反而是負債。

注意事項

⚠️ 別讓聽眾以為「真實專案就該一次塞八個模式」。 Lexi 是 GoF 精心設計的教學案例,每個問題都「剛好」對上一個模式,現實沒這麼乾淨。這集的價值是讓你看見模式從哪種問題長出來,不是叫你照抄。真實開發裡,大多數程式碼根本不需要任何模式——記得 EP1 講的 patternitis。

⚠️ Abstract Factory 和 Bridge 真的很容易搞混,務必把『差異』講清楚。 一句話區分:Abstract Factory 管「建出哪一家族的物件」,Bridge 管「抽象和實作兩條繼承軸如何解耦」。很多人到工作好幾年還分不清,這集是最好的釐清時機。

專家補充

💡 整章八個模式,反覆指向同一句話:『encapsulate the concept that varies』(封裝會變的概念)。 Lexi 案例裡,會變的依序是:結構(Composite)、排版演算法(Strategy)、UI 裝飾(Decorator)、外觀風格(Abstract Factory)、視窗系統(Bridge)、使用者操作(Command)、遍歷與分析(Iterator/Visitor)。把這條主線拎出來,這集就有了統一的敘事骨架——模式不是八個招式,是同一個原則的八種應用

💡 Command + Undo 這段,是現代軟體的隱形基礎建設。 你用的每個編輯器的 Ctrl+Z、資料庫的 transaction log、訊息佇列的 job、前端 Redux 的 action——骨子裡都是「把操作物件化」這個 Command 觀念。這集可以順帶讓聽眾「啊」一聲:原來我天天在用。

💡 「遍歷」和「對遍歷到的東西做事」應該分開——這是 Iterator + Visitor 雙人組教的更深一層觀念。它其實預告了函數式裡 map/fold 的思路:資料結構提供走訪,操作獨立傳入。GoF 在 1994 年用物件導向的笨重方式(雙重分派)達成這件事,FP 後來用 first-class function 輕鬆做到——這個對照到 EP11 會收。

討論問題

🎙️ 開場鉤子:「一個文字編輯器,能有多複雜?GoF 說:複雜到光是把它設計好,就要用上八個設計模式。今天我們就拆開這台機器,看看每個齒輪長什麼樣。」

🎙️ 自問自答:「為什麼 GoF 要花一整章講一個編輯器,而不是直接進模式目錄?」——因為他們要你先相信一件事:模式是被問題逼出來的,不是被發明出來的。你只有先感受到痛,才會珍惜止痛藥。

🎙️ 帶走的一題:「回想你最近做的一個功能,它解決的『會變的東西』是什麼?是演算法、是結構、還是某個外部依賴?光是能說清楚『這裡會變的是 X』,你就已經站在設計模式的入口了。」

更大範圍關聯

  • 本章是全書的「縮影」:Lexi 用到的八個模式,跨越創建、結構、行為三大類,等於整季的預告片。後面每集深講時,都可以回扣「還記得 Lexi 裡的那個問題嗎」。
  • 與 Refactoring 的接點:Fowler《Refactoring to Patterns》的精神就是這章的反向操作——先有混亂的編輯器原型,再一步步重構出這八個模式。GoF 給終點,Fowler 給路徑(見 [軟體工程領域切分藍圖])。
  • WYSIWYG 編輯器的設計史:Lexi 的 Glyph/Composite 思路,後來幾乎是所有 GUI 框架(從 Java Swing 到瀏覽器 DOM、到 Flutter 的 Widget tree)的共同祖型。這章其實在教你讀懂所有現代 UI 框架的底層結構。
  • 與 A Philosophy of Software Design 的張力:Ousterhout 會問「Lexi 真的需要這麼多層抽象嗎?」——這是健康的提問,提醒我們案例是教學用的理想化版本。

錄製建議

  • 建議時長:約 25-28 分鐘(這集模式密度高)。配比:開場+Composite/Strategy/Decorator 約 12 分(結構與行為的三連打)、Abstract Factory vs Bridge 對照約 7 分(重點釐清)、Command+Undo 約 5 分、Iterator/Visitor 約 4 分。
  • 討論策略:用「一台機器拆開來看齒輪」當貫穿比喻,每個問題都先講「痛點」再講「模式怎麼救」,最後一定回扣「會變的是什麼」。八個模式一次講會資訊過載,所以這集只求建立直覺與鉤子,細節留給後面各集——明確跟聽眾說「這些模式我們之後會一個個深挖」。
  • 提醒聽眾:這集是「預告片」,聽不懂某個模式的細節很正常,後面有專集。