子類別老是拒絕父類別的方法,是哪裡錯了?

Medium★★★★Studied

FromRefactoring for Software Design SmellsAgile Principles, Patterns, and Practices

Concepts practiced📏 Liskov 替換原則 (LSP)📏 介面隔離原則 (ISP)📏 組合優於繼承 (Composition over Inheritance)

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

📚 From the Books

子類別老是拒絕父類別的方法,是哪裡錯了?ProblemMedium反模式辨識

一個子型別繼承了父型別,卻在覆寫時「造反」——某些父方法它拋 UnsupportedOperationException、給空實作、或回傳錯誤值來表示「我不支援」。IS-A 關係看似成立,替換卻會爆炸。這是 Rebellious Hierarchy(叛逆繼承 / Refused Bequest),怎麼辨識、怎麼解?

🎯 Scenario

open class CallSite {
    open fun setTarget(t: Target) { /* 可變更目標 */ }
    fun getTarget(): Target = TODO()
}

// 常數呼叫點:target 永久不變 → 被迫拒絕父類別的 setTarget
class ConstantCallSite : CallSite() {
    override fun setTarget(t: Target): Unit =
        throw UnsupportedOperationException("constant call site")   // 造反!
}

壞味道:Rebellious Hierarchy——子型別拒絕繼承來的方法(拋例外 / 空實作 / 印 “should not implement” / 回傳錯誤值)。直接違反 里氏替換原則:拿子型別替換父型別會失敗。

🧠 Intuition

TIP

先別看下面。問兩個問題:(1) 子型別和父型別真的是 IS-A 嗎? (2) 被拒絕的方法,是只有部分子型別不需要,還是全部子型別都不需要? 這兩題的答案決定了該往哪個方向解。

⚖️ Reference Design

先分辨:Rebellious 還是 Broken?
  • Rebellious Hierarchy:父子有 IS-A 關係,但子型別拒絕某些方法(本頁)。
  • Broken Hierarchy:父子根本沒有 IS-A 關係,繼承本身就錯了(用了繼承只為重用程式碼)。
  • 別名:Refused bequest(拒絕遺贈)、Naughty childrenPremature interface abstraction(方法在階層中位置太高,被部分子型別拒絕)。
解法:看『誰不需要這方法』決定往哪搬
// 方案:把可變更目標的能力下放到中介型別,常數呼叫點就不必再拒絕
open class CallSite { fun getTarget(): Target = TODO() }
open class ChangeableCallSite : CallSite() { open fun setTarget(t: Target) { /* ... */ } }

class ConstantCallSite : CallSite()             // 不再有 setTarget,無從拒絕
class MutableCallSite  : ChangeableCallSite()   // 需要才繼承可變能力
  • 被拒方法只與部分子型別相關move method 把它下放到那些子型別,或引入中介型別容納共通子集。
  • 被拒方法所有子型別都不需要 → 直接從父型別 remove method
  • 成因常是 Swiss-knife 父類別(把所有可能功能塞進父類,逼子類拒絕)——這也是 ISP 要解的:介面別胖到讓實作者被迫拒絕。
  • 若父子其實不是 IS-A,改用組合委派,而非繼承。

NOTE

書中提醒:真實設計常沒有完美解(每個方案都有代價,如變成 Incomplete/Unfactored Hierarchy),要在權衡下選。

務實例外:刻意引入的『造反』

書中列了幾個有意違反、且被明確記錄的合理情況:

  • 尚未實作RTFEditorKit.write() 拋例外,因為 RTF 支援是 work-in-progress,拒絕只是暫時占位。
  • 為效能DefaultListCellRendererrepaint()/validate() 覆寫成 NOP,避免無謂成本。
  • 為避免類別爆炸UnmodifiableList 拒絕 add()/remove()——違反 LSP,但是 Collections API 的有意取捨。
  • Composite 樣式:讓 client 透明處理 leaf 與 composite 時,leaf 的 addComponent() 無意義,三種解法都有代價,設計者有意識地選擇承受此味道。

🔑 Takeaways

✍️ My Notes

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

📖 Further Reading