想複用或擴充行為時,優先「持有一個物件並委派」而不是「繼承一個父類」。繼承是編譯期綁死的強耦合,組合是執行期可換的弱耦合。GoF 全書的隱性主軸。
🧠 When to Use
TIP
訊號:你正想 extends 一個類別只為了「拿到它幾個方法」,或繼承樹已經三四層深、改父類會牽動一片子類。
- 想複用行為,但兩者不是真正的 is-a 關係(只是想借功能 → 該用 has-a)。
- 行為要能在執行期替換 / 組合(這正是 Strategy、Decorator 成立的基礎)。
- 繼承導致的脆弱基底類別問題開始出現:改父類就到處壞。
⚖️ Structure & Variants
繼承 vs 組合 — 同一需求兩種寫法
// ❌ 繼承:Duck 想要會飛,就 extends 一個 FlyingThing —— 綁死、不能換
open class FlyingThing { open fun fly() = println("flap") }
class Duck : FlyingThing() // 那橡皮鴨不會飛怎麼辦?整棵樹開始長例外
// ✅ 組合:把「飛行行為」抽成可注入的策略
fun interface FlyBehavior { fun fly() }
class Duck(private val flying: FlyBehavior) {
fun performFly() = flying.fly()
}
val mallard = Duck { println("flap") }
val rubber = Duck { /* 不會飛 */ } // 換一個行為即可,不動類別樹- 經典「橡皮鴨問題」:用繼承時例外情況逼你在樹上到處 override;用組合時只是換注入物件。
什麼時候繼承才對
- 真正的 is-a 且子型別可完全替換父型別(Liskov 替換原則)。
- 想共用「固定流程骨架、只變其中幾步」→ 那是 Template Method,繼承是恰當工具。
- 介面繼承(implements)幾乎總是好的;要小心的是實作繼承(extends 具體類)。
⚠️ Misuse & Anti-patterns
- 為複用而繼承:只因為父類「剛好有那個方法」就 extends,引入一堆不想要的 API 與耦合。
- 把組合做成貧血委派地獄:每個方法都只是
inner.x()的轉手,毫無新增——這時該問是不是介面切錯了。 - 矯枉過正:固定流程、真 is-a 的情況硬拆成組合,反而失去 Template Method 的清楚骨架。原則是「優先」,不是「禁止繼承」。
🔑 Takeaways
- 繼承=編譯期綁死的強耦合;組合=執行期可換的弱耦合,預設選後者。
- Strategy / Decorator / Bridge 等一票模式,骨子裡都是這條原則的具體化。
- 例外:真 is-a 且可替換、或要共用固定流程骨架(Template Method)時,繼承才恰當。
No notes yet — jot your takeaways or Q&A here.