當子型別直接而且間接繼承同一父型別——多餘的繼承路徑出現——就是 Multipath Hierarchy。

核心定義#

與**鑽石階層(diamond hierarchy)**的差別:

  • 鑽石階層:子型別透過多條合理路徑抵達同一祖先,每條路徑都有意義(如 ByteChannel ← ReadableByteChannel ← ChannelByteChannel ← WritableByteChannel ← Channel)。
  • Multipath Hierarchy:至少有一條路徑是多餘的,移除不會改變語意。

Figure 6.42: Channel 與 ByteChannel 之間的鑽石階層

常見成因#

  • 忽略既有繼承路徑:定義型別時沒注意祖先已經繼承了某個介面,重複再實作一次。深階層中尤其容易發生。

範例:ConcurrentLinkedQueue#

java.util.concurrent.ConcurrentLinkedQueue

  • 繼承 java.util.AbstractQueueAbstractQueue 已實作 Queue
  • ConcurrentLinkedQueue 又自行 implement Queue——多此一舉。

Figure 6.43: Queue 與 ConcurrentLinkedQueue 之間多餘的繼承路徑

案例:Gadget 階層的不完整重構#

一個 GUI 應用 TextGadget 既繼承 GadgetBase 又實作 ISupportInitialize。後來團隊把 ISupportInitialize 上提到 GadgetBase,預期所有 gadget 不再各自實作——但漏改了部分 gadget 類別:它們同時透過 GadgetBase 與直接 implement ISupportInitialize 兩條路徑繼承介面。

Figure 6.44: TextGadget 類別的階層

Figure 6.45: TextGadget 階層原本預期的重構

Figure 6.46: TextGadget 不完整重構後的設計

兩個教訓:

  • 重構必須完整——只做一半就會留下 Multipath Hierarchy。
  • 設計變更必須廣播給整個團隊——文件化、團隊規範、code review 全到位。

建議重構#

移除多餘的繼承路徑。

  • ConcurrentLinkedQueue:刪除「直接 implement Queue」的宣告即可,因為它已經透過 AbstractQueue 繼承了 Queue

Figure 6.47: 對範例建議的重構

對品質屬性的衝擊#

  • 理解性:多餘路徑使階層複雜,提高認知負擔。
  • 可靠性:開發者可能不知 GadgetBase 已提供標準實作 → 在子類重新提供(甚至不提供)→ 執行期錯誤。

別名#

  • Degenerate inheritance:子類別直接與間接繼承同一父類別。
  • Repeated inheritance:類別經多條路徑重複繼承同一祖先。

實務考量:刻意引入冗餘路徑#

軼事:一個 Java 專案的開發者故意讓低階子類別也明確 implements Serializable——因為複雜階層中追蹤是否可序列化太麻煩。設計者犧牲了此原則,換取查找的便利性。 教訓:當壞味道的代價低於它帶來的好處時,有意識地接受也是合理的設計選擇。