當子型別**「拒絕」**了從父型別繼承的方法時,就出現 Rebellious Hierarchy。

核心定義#

子型別與父型別在概念上仍是 IS-A 關係,但子型別覆寫父方法的方式違反了這份契約:

  • 拋例外拒絕呼叫。
  • 提供空(NOP)實作。
  • 印出「should not implement」訊息。
  • 回傳錯誤值表示不支援。

違反 Liskov Substitution Principle

與 6.8 節 Broken Hierarchy 的差別:

  • Rebellious Hierarchy:父子有 IS-A 關係,但子型別拒絕某些方法。
  • Broken Hierarchy:父子沒有 IS-A 關係,繼承本身就錯了。

常見成因#

  • 由現有具體類別衍生新階層:原本的具體類別不適合做父型別。
  • Swiss-knife 父類別:設計者把所有可能的功能塞進父類別,子類別被迫拒絕不相關的方法。

範例#

範例 1:CallSite#

java.lang.invoke.CallSite 的子類:

  • ConstantCallSite:target 永久不變setTarget()UnsupportedOperationException
  • MutableCallSiteVolatileCallSite:target 可變。

結果父類別的 setTarget() 在某個子型別中無效。

Figure 6.26: java.lang.invoke.CallSite 的繼承階層

範例 2:Toolkit / HeadlessToolkit#

java.awt.Toolkit 的子類 HeadlessToolkit 沒有顯示器、鍵盤、滑鼠——大量方法(createWindow()getGlobalCursorManager() 等)拋 HeadlessException

範例 3:Iterator.remove()#

java.util.Iteratorremove()java.util.Scanner 中被拋 UnsupportedOperationException 拒絕——因為 Scanner 是 read-only 的 tokenizer。

建議重構#

  • 被拒絕的方法只與部分子型別相關move method 把方法搬到那些子型別。
  • 被拒絕的方法所有子型別都不需要remove method 從父型別移除。
  • 也可引入中介型別容納共通子集的需求。

範例 1:CallSite 的三種方案#

選項做法代價
1CallSite 移除 setTarget()CallSite 變成 Incomplete Abstraction
2把兩個方法都搬到子類別出現 Unfactored Hierarchy
3引入 ChangeableCallSite 中介類別,將 setTarget() 放入getTarget() 仍在子類重複

Figure 6.27: 對 CallSite 建議的重構(選項 1)

Figure 6.28: 對 CallSite 建議的重構(選項 2)

Figure 6.29: 對 CallSite 建議的重構(選項 3)

真實設計裡常常沒有完美解——必須在多種權衡下選擇。

範例 2:Toolkit 的重構#

Toolkit 拆為 Toolkit(無設備相關方法)→ DefaultToolkit(含設備方法)/HeadlessToolkit(純 headless)。

Figure 6.30: 對 Toolkit 階層建議的重構

範例 3:Iterator 的重構#

引入 ReadOnlyIterator(不含 remove())。java.util.Scanner 改實作 ReadOnlyIterator

對品質屬性的衝擊#

  • 理解性:客戶端會被「替換失敗」嚇到。
  • 可變更性:客戶端被迫綁定具體子型別。
  • 可擴充性:新加子類別也必須拒絕同樣方法。
  • 可重用性:行為不一致使重用困難。
  • 可靠性:拋出未處理的例外可能讓應用程式崩潰。

別名#

  • Refused bequest:拋例外、空實作、或客戶端只用子類引用。
  • Refused parent bequest:私有繼承介面方法或以空方法覆寫。
  • Naughty children:子類別不接受父類別接受的所有訊息。
  • Premature interface abstraction:方法在階層中位置太高,被部分具體子類拒絕。

實務考量:刻意引入的 Rebellious Hierarchy#

書中提供三個有意違反此原則的合理例子:

尚未實作的功能#

RTFEditorKit.write()IOException,因為 RTF 支援是「work-in-progress」。文件直接坦承「The RTF support was not written by the Swing team. In the future we hope to improve」——拒絕是暫時的占位。

Figure 6.31: RTFEditorKit 的繼承階層

為效能而覆寫為 NOP#

DefaultListCellRenderer 等覆寫 invalidate()validate()repaint() 為空方法,是為了避免無謂的執行成本。設計者明確記錄此決策。

為避免類別爆炸#

JDK 7 的 UnmodifiableListUnmodifiableMap 拒絕 add()set()remove()——違反 LSP。但這是 Java Collections API 為避免介面與類別爆炸做的有意取捨。

案例:Composite 樣式#

Composite pattern 的關鍵在於讓 client 透明地處理 leaf 與 composite。父類別 Component 中的 addComponent() 對 leaf 無意義,三種解法都有代價:

Figure 6.32: Composite 設計樣式的結構

  • 在 leaf 中拒絕該方法 → Rebellious Hierarchy。
  • addComponent() 移到 Composite → 客戶端必須做型別檢查 → 失去透明性
  • Component 中提供 NOP 預設實作 → 不會壞,但 bug 會被吞掉。

設計者有意識地引入此壞味道,以保留樣式的核心價值。