第 6 / 11 部 待錄

結構型模式II-組合與裝飾

《Design Patterns 設計模式(GoF)》Podcast 準備稿:結構型模式 II — Composite、Decorator 與 Flyweight

書名: 設計模式 Design Patterns(Elements of Reusable Object-Oriented Software) 作者: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides(Gang of Four) 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|Design Patterns.樹狀組合與層層包裹:Composite/Decorator/Flyweight (6/11) 涵蓋範圍: Composite、Decorator(深講)+ Flyweight(速覽)

背景速覽

這集是結構型模式的第二棒,主角是一對「遞迴組合雙人組」:**Composite(組合)**和 Decorator(裝飾)。它們共享同一個底層技巧——讓「包裝物件」和「被包裝物件」實作相同介面,於是可以無限遞迴巢套——但目的完全不同:Composite 是為了「把多個東西當一個東西處理」,Decorator 是為了「動態替一個東西加職責」。

最後再速覽 Flyweight(享元),一個專門解決「物件太多、記憶體爆掉」的省記憶體模式,它和 Composite 常常搭配出現。這集會把「遞迴組合」這個威力強大的觀念講透,它是現代所有 UI 框架的底層心法。

一句話重點

Composite 把物件組成樹,讓 client 不必區分「單一物件」和「一群物件」;Decorator 用「一個包一個」的方式,在 run-time 動態疊加職責、避免子類別爆炸;兩者技巧相同(遞迴組合 + 相同介面),但 Composite 在「聚合」,Decorator 在「加料」。

值得討論的重點

1. Composite:讓「葉子」和「樹枝」長一樣

Intent:把物件組成樹狀結構表達「部分—整體」階層,讓 client 能用一致的方式對待個別物件和組合

動機(呼應 EP2):繪圖程式裡,使用者用簡單圖形組出複雜圖形、群組化、再群組化。爛做法是「基本圖形」和「容器」用不同類別,於是 client 到處要判斷「這是單個還是一群」。

Composite 的解法:一個抽象 Graphic,同時代表基本元素和容器。LineRectangleLeafPictureComposite,內含一組 Graphic,它的 Draw 遞迴呼叫子元件的 Draw。因為 Picture 也是 Graphic,它能再裝別的 Picture——無限遞迴。

核心價值:client 不用知道它處理的是 Leaf 還是 Composite,消除型別判斷和 if/else 分支。 這簡化 client 邏輯的威力非常大。

一個經典爭議點要講(GoF 自己提的):管理子元件的操作(AddRemove)該放哪?

  • 放在 Component(最上層)→ 透明性好(葉子和容器介面一致),但對 Leaf 呼叫 Add 在語意上是錯的。
  • 放在 Composite安全性好,但 client 又要區分型別了。

GoF 的傾向是偏向透明性,但讓 Leaf 的 Add 預設拋例外而非靜默忽略。這個「透明 vs 安全」的取捨,是 Composite 設計的核心張力。

2. Decorator:一層一層包出新功能

Intent動態地替個別物件(不是整個類別)附加職責,提供比子類別化更彈性的擴展方式。別名 Wrapper(跟 Adapter 同名,但完全不同!)。

動機:GUI 元件要能加邊框、加捲軸,而且可能隨時增減。用繼承做會子類別爆炸(BorderedScrollableTextView…)。Decorator 的做法是用另一個物件「包住」原元件:ScrollDecorator 包住 TextView 加捲動,BorderDecorator 再包住它加邊框。每層 decorator 都跟被裝飾物件實作相同介面,對 client 透明,把請求轉發進去、轉發前後加自己的動作。

兩大優點:比靜態繼承彈性(職責可 run-time 動態加減,同種裝飾還能套兩次——雙重邊框);避免高層類別功能臃腫(pay-as-you-go,從簡單類別出發、需要才包)。

代價要誠實講:物件身分問題(裝飾後的物件「不等於」原物件,別依賴 object identity)、以及一堆長相相似的小物件,除錯時容易眼花。

3. Composite vs Decorator:技巧相同,意圖相反

這是本集的核心對照,務必講透。GoF 自己說:Decorator 可以視為「只有一個子元件的退化 Composite」。 它們都用「相同介面 + 遞迴組合」。但:

  • Composite 的重點是聚合——把 N 個東西當一個樹來處理。
  • Decorator 的重點是加料——不聚合,只是替一個東西層層加職責。

一句話:Composite 在「收編」,Decorator 在「加值」。

順帶提一個三角關係(GoF 也提):Decorator 改外皮、Strategy 改內臟——當 Component 已經很龐大、不適合再包一層 decorator 時,可以改用 Strategy 把行為委派出去。這條留個鉤子,行為型那幾集會接。

4. Flyweight:物件太多時,把「可共享的」抽出來共用(速覽)

Intent:透過共享來有效率地支援大量細粒度物件

動機:文件編輯器若為每個字元都建一個物件,幾十萬個物件記憶體吃不消。Flyweight 的關鍵是把物件狀態拆兩半:

  • Intrinsic state(內在狀態):與 context 無關、可共享(如「字元碼 A」)。
  • Extrinsic state(外在狀態):依 context 而定、不可共享,由 client 每次傳入(如「這個 A 在文件第幾頁、什麼字型」)。

於是一篇文件無論多長,只需要約 100 個字元 flyweight 物件(ASCII 集大小),位置和字型由外部傳入。這是空間換時間的取捨:省記憶體,但增加「每次傳遞/計算外在狀態」的執行成本。

要點一句:共享的 flyweight 不能存父引用——所以它和 Composite 結合時,父引用得當外在狀態傳入。Flyweight 在現代用途:字串駐留(string interning)、Integer 快取、遊戲裡的粒子/方塊、連線池的某些變體。

注意事項

⚠️ 「Wrapper」這個別名同時屬於 Adapter 和 Decorator,但兩者天差地遠——別讓聽眾混淆。 Adapter 是改介面(讓不相容的能合作);Decorator 是不改介面、只加職責(對 client 透明)。同樣是「包一層」,一個目的是翻譯、一個目的是加料。上集(EP5)的 Adapter 和這集的 Decorator 剛好可以對照釐清。

⚠️ Decorator 疊太多層會變成除錯地獄。 它的彈性是有代價的:一個請求可能穿過五六層 wrapper 才到真身,stack trace 又臭又長、一堆長相相同的小物件。彈性和可讀性永遠在拉鋸——別因為「Decorator 很優雅」就無腦疊。

專家補充

💡 「遞迴組合」是現代 UI 框架的共同心法,學會 Composite 等於拿到一把萬能鑰匙。 瀏覽器的 DOM、React/Vue 的元件樹、Flutter 的 Widget tree、iOS 的 view hierarchy——全都是 Composite。它們都讓「一個按鈕」和「一整個畫面」實作相同的「可渲染」介面,於是可以無限巢套。你只要理解 Composite,就理解了所有 UI 框架的骨架。

💡 Java I/O 是 Decorator 最有名(也最惡名昭彰)的教科書範例。 new BufferedReader(new InputStreamReader(new FileInputStream(...)))——一層包一層加緩衝、加編碼轉換。它完美示範了 Decorator 的彈性,但也完美示範了它的副作用:寫起來囉嗦、新手看了一頭霧水。這個例子拿來講 Decorator 的「優雅與代價」兩面,再生動不過。

💡 Flyweight 在當代其實常常『隱形』了。 很多語言把它內建:Java 的 Integer.valueOf 對 -128~127 做快取、字串常量池自動 interning。你不用自己寫 Flyweight,語言/runtime 幫你做了。這又是 EP1 主題的延伸——有些模式進化成了語言或執行環境的內建機制,你享受它的好處卻不必親手實作。

討論問題

🎙️ 開場鉤子:「Composite 和 Decorator,用的是同一個底層魔法——讓包裝物和被包裝物長得一模一樣,於是能無限套娃。但一個是為了『把很多東西當一個』,一個是為了『幫一個東西層層加料』。今天我們把這對雙胞胎拆開來看。」

🎙️ 自問自答:「Composite 和 Decorator 結構這麼像,怎麼分?」——看意圖。Composite 在收編(多個變一個樹),Decorator 在加值(一個東西疊職責)。GoF 甚至說 Decorator 就是『只有一個小孩的退化 Composite』。

🎙️ 帶走的一題:「打開你最熟的 UI 框架——React、Flutter、Swing 都行——看看它的元件樹。那個『按鈕和整個畫面都實作同一個介面』的設計,就是 Composite。你天天在用,今天終於知道它的名字了。」

更大範圍關聯

  • Composite 與所有 UI 框架:DOM、React/Vue 元件樹、Flutter Widget、iOS view hierarchy 都是 Composite,它是 GUI 工程的共同地基(見 [軟體工程領域切分藍圖] 的前端/UI 支脈)。
  • Decorator 與「組合勝過繼承」:它是 GoF 三大原則中「favor composition」最直接的化身——用包裹取代繼承來擴展,與《A Philosophy of Software Design》對「淺層 vs 深層模組」的討論可對照(過多薄 decorator 是否值得?)。
  • Flyweight 與內建優化:字串 interning、Integer 快取是 Flyweight 的語言級實現,呼應「模式被語言吸收」的主題。
  • 與 Refactoring 的接點:Fowler 的「Replace Inheritance with Delegation」等重構手法,正是從繼承走向 Decorator/Composite 的路徑(見 [軟體工程領域切分藍圖])。

錄製建議

  • 建議時長:約 23-25 分鐘。配比:Composite 約 8 分(含透明 vs 安全的取捨)、Decorator 約 8 分(含 Java I/O 範例)、Composite vs Decorator 對照約 3 分、Flyweight 速覽約 4 分。
  • 討論策略:本集核心是「同一技巧、相反意圖」這個對照,建議先各自講透,再並排比較,用「收編 vs 加值」一句話收。Decorator 一定要帶 Java I/O 這個又愛又恨的範例,聽眾共鳴最強。Flyweight 點到為止,重點放在「它常被語言內建」的觀念,不必深挖實作。
  • 強力建議用「打開你的 UI 框架看元件樹」當行動呼籲,讓抽象的 Composite 立刻接地。