有個基底型別(資料流),要為它加上壓縮、加密這些可選、可疊加的加工。用繼承做,每種組合都得開一個子類別——「又壓縮又加密」再開一個,子類別數量開始爆炸,而且組合方式在編譯期就被寫死。要怎麼重構?
🎯 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 講的結構。
為什麼它解掉了爆炸:組合優於繼承、執行期堆疊
把「實作功能」與「如何組合」拆開,建鏈邏輯要與 Client 解耦
Decorator 帶來一個乾淨的拆解:
- 如何實作每個附加功能——交給各個 decorator,各自獨立、可重用、可任意排序。
- 如何把這些功能組合成一條鏈——抽出去交給工廠依配置資訊建鏈。
建鏈邏輯一定要與 Client 分離,否則 decorator 的彈性會被打折——Client 不應該知道有這些裝飾物存在。
取捨與誤用:順序敏感、小物件過多、別為單一職責過度包裝
- 裝飾順序很重要:例如加密與過濾都可當持久化前的裝飾,但先加密再過濾就會出問題。最理想是保證各裝飾類彼此獨立,讓它們能以任意順序組合。
- 大量外觀相似的小物件:差異只在連接方式,可能增加學習與除錯難度;被裝飾後的物件也不再是原物件,不應依賴物件身分。
- 只需要加一種職責時,可省略抽象 Decorator 類別,別為單一功能疊一堆包裝。
- 別把「結構」誤當成模式本身:當各 decorator 由不同團隊維護、異常處理參差時,整條鏈可能一起炸掉;改由一個 collection 物件呼叫各裝飾並負責例外捕獲,鏈式結構雖已不存在,Decorator 的精神(多個可選功能、可組合、不影響 Client)仍在。
🔑 Takeaways
- 「基底 + 多個可選附加功能」用繼承表達,會因為要覆蓋所有組合而導致子類別數量爆炸,且組合在編譯期被寫死。
- 重構成 Decorator:用符合同一介面的包裝物件持有內部元件,先加行為再委派,在執行期堆疊——組合優於繼承,n 個裝飾類取代 2ⁿ 個子類。
- 把「實作功能」與「如何組合」拆開,建鏈邏輯與 Client 解耦;並留意順序敏感、小物件過多,別為單一職責過度包裝。
No notes yet — jot your takeaways or Q&A here.