用繼承組合功能,子類別爆炸了要怎麼解?

Medium★★★★Studied

From大話設計模式Design Patterns (GoF)Design Patterns Explained

Concepts practiced🧱 裝飾者 (Decorator)📏 組合優於繼承 (Composition over Inheritance)📏 開放封閉原則 (OCP)

Convention: read the requirements & constraints first, sketch your own design, then expand the reference to compare.

📚 From the Books

用繼承組合功能,子類別爆炸了要怎麼解?ProblemMedium重構情境

有個基底型別(資料流),要為它加上壓縮、加密這些可選、可疊加的加工。用繼承做,每種組合都得開一個子類別——「又壓縮又加密」再開一個,子類別數量開始爆炸,而且組合方式在編譯期就被寫死。要怎麼重構?

🎯 Scenario

open class FileStream(val path: String) {
    open fun write(data: String) { /* 真正落檔 */ }
}

// 想加壓縮 → 開一個子類
class CompressedFileStream(path: String) : FileStream(path) {
    override fun write(data: String) = super.write(compress(data))
}

// 想加加密 → 再開一個子類
class EncryptedFileStream(path: String) : FileStream(path) {
    override fun write(data: String) = super.write(encrypt(data))
}

// 想「又壓縮又加密」→ 只能再開一個組合子類……
class CompressedEncryptedFileStream(path: String) : FileStream(path) {
    override fun write(data: String) = super.write(encrypt(compress(data)))
}
// 再加第三種加工,要覆蓋所有組合的子類數量就開始成倍數暴增

壞味道:可選加工彼此獨立、又要能任意疊加,用子類別化來擴展根本不切實際——可能的獨立擴展太多,會導致子類別數量爆炸。而且用繼承做,「要不要某個加工」的選擇是靜態的、在編譯期就決定了,客戶端無法在執行期按需要、按順序組裝。

🧠 Intuition

TIP

先別看下面。問自己:壓縮、加密這些加工,彼此之間真的是「是一種 FileStream」(is-a)的關係嗎?還是它們只是一層層包在資料流外面、可選、可疊、可換順序的功能?如果換成「用一個物件包住另一個物件」,會發生什麼事?你會用哪個 pattern?

⚖️ Reference Design

答案:重構成 Decorator,用同介面的包裝物件在執行期堆疊

不要用繼承把每種組合寫成一個子類,而是用另一個符合相同介面的物件去「包裹」原始元件——這個包裹物件就是 decorator。它持有內部元件的引用,在把請求轉發給內部元件的前後,執行自己額外的操作:

interface Stream {                                   // Component:共同介面
    fun write(data: String)
}

class FileStream(val path: String) : Stream {        // ConcreteComponent:鏈尾
    override fun write(data: String) { /* 真正落檔 */ }
}

abstract class StreamDecorator(private val inner: Stream) : Stream {
    override fun write(data: String) = inner.write(data)   // 預設把請求轉發給內部元件
}

class CompressDecorator(inner: Stream) : StreamDecorator(inner) {
    override fun write(data: String) = super.write(compress(data))  // 先加行為再委派
}

class EncryptDecorator(inner: Stream) : StreamDecorator(inner) {
    override fun write(data: String) = super.write(encrypt(data))
}

客戶端在執行期把要用的功能一層層串起來:

val out = EncryptDecorator(CompressDecorator(FileStream("a.dat")))
out.write("hello")   // 先壓縮、再加密、最後落檔

裝飾者符合被裝飾元件的介面,對客戶端是透明的;每個裝飾物件只關心自己的功能,不需要知道自己如何被加進物件鏈裡。這正是機制頁 Decorator 講的結構。

為什麼它解掉了爆炸:組合優於繼承、執行期堆疊
  • 關鍵是 組合優於繼承:透過物件組合而非類別繼承來擴展功能。原本要用 2ⁿ 個組合子類才能覆蓋的情況,現在只要 n 個彼此獨立的裝飾類,就能自由混搭。
  • 職責可在執行期動態添加、移除;裝飾物件還能遞迴巢套,堆疊任意數量的職責,同一種裝飾甚至能套用兩次。
  • 採「按需付費」(pay-as-you-go):從簡單類別開始,用 decorator 逐步加功能,避免把所有選項都塞進一個功能臃腫的高層類別。
  • 新增一種加工只需新增一個裝飾類,既有的元件與其他裝飾類都不必改動 → 呼應 OCP
把「實作功能」與「如何組合」拆開,建鏈邏輯要與 Client 解耦

Decorator 帶來一個乾淨的拆解:

  • 如何實作每個附加功能——交給各個 decorator,各自獨立、可重用、可任意排序。
  • 如何把這些功能組合成一條鏈——抽出去交給工廠依配置資訊建鏈。

建鏈邏輯一定要與 Client 分離,否則 decorator 的彈性會被打折——Client 不應該知道有這些裝飾物存在。

取捨與誤用:順序敏感、小物件過多、別為單一職責過度包裝
  • 裝飾順序很重要:例如加密與過濾都可當持久化前的裝飾,但先加密再過濾就會出問題。最理想是保證各裝飾類彼此獨立,讓它們能以任意順序組合。
  • 大量外觀相似的小物件:差異只在連接方式,可能增加學習與除錯難度;被裝飾後的物件也不再是原物件,不應依賴物件身分。
  • 只需要加一種職責時,可省略抽象 Decorator 類別,別為單一功能疊一堆包裝。
  • 別把「結構」誤當成模式本身:當各 decorator 由不同團隊維護、異常處理參差時,整條鏈可能一起炸掉;改由一個 collection 物件呼叫各裝飾並負責例外捕獲,鏈式結構雖已不存在,Decorator 的精神(多個可選功能、可組合、不影響 Client)仍在。

🔑 Takeaways

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading