軟體設計

組合(Composition)vs 繼承(Inheritance):「不要使用繼承」這句話的真正含義

學習物件導向時,一定會遇到這句話:「優先使用組合,而不是繼承(Favor composition over inheritance)。」

閱讀 4 分鐘
組合(Composition)vs 繼承(Inheritance):「不要使用繼承」這句話的真正含義 封面圖

組合 vs 繼承:「不要使用繼承」這句話的真正含義

學習物件導向時,一定會遇到這句話:「優先使用組合,而不是繼承(Favor composition over inheritance)。」

先說結論,這句話並不是「絕對不要使用繼承」,而是「不要為了重複使用程式碼而濫用繼承」。繼承仍然是有效的工具,但在需要重複使用的情況下,大多數時候組合是更安全的選擇。

本文會結合我的經驗,說明兩者在實際程式碼中的差異,以及什麼時候該選擇哪一種。

繼承與組合有什麼不同?

先用非常簡單的方式區分一下。

繼承是「~是~(is-a)」的關係。比如狗是動物。子類別會直接繼承父類別的功能。

組合則是「~擁有~(has-a)」的關係,例如汽車擁有引擎。它把具備所需功能的物件放在內部,再委派工作給該物件。

用程式碼來看會更容易理解。以下是透過繼承取得功能的方式。

// 繼承: Stack會 NSMutableArray繼承所有方法
class Stack: NSMutableArray {
    func push(_ o: Any) { add(o) }
    func pop() -> Any {
        let o = lastObject!
        removeLastObject()
        return o
    }
}

問題是,這樣一來 Stack 連不希望提供給外部的 insert(_:at:)、removeObject(at:) 等方法也會全部暴露出去。

以下是將同樣的功能改用組合實作的樣子。

// 組合:只挑選必要的功能並進行委派
class Stack {
    private var list: [Any] = []
    func push(_ o: Any) { list.append(o) }
    func pop() -> Any { list.removeLast() }
}

我們把陣列放在內部,只對外開放必要的操作。Stack 真正想做的事就只剩下來了。

繼承取得全部功能,組合只取得必要功能
繼承取得全部功能,組合只取得必要功能

「不要使用繼承」這句話的真正含義

這句話之所以出現,是因為繼承有兩個代表性的缺點。

第一,封裝會被破壞。子類別會依賴父類別的內部實作。父類別程式碼一改,原本正常的子類別也可能突然出錯。

第二,耦合度會變得過高。父類別與子類別在編譯時期緊密綁定,之後很難改變兩者的關係。

繼承不是重複使用程式碼的工具,而是定義型別的工具。

這句話就是核心。當你只是因為想重複使用程式碼而使用繼承時,關係就開始變得複雜。

正方形與矩形就是著名的例子。從數學上來說,正方形是矩形,因此看起來可以使用繼承。但一旦繼承了矩形「分別變更寬度與高度」的功能,正方形就不再是正方形了。

即使看起來像 is-a 關係,只要無法連行為都完全替代,繼承就會成為陷阱。


那麼,什麼時候可以使用繼承?

繼承並不是絕對不好。只要同時滿足以下條件,使用繼承反而會很簡潔。

  1. 是真正的 is-a 關係嗎:子類別永遠都是父類別的一種嗎?
  2. 遵守里氏替換原則嗎:把子類別放到父類別的位置,是否完全沒有問題?
  3. 父類別是否以可繼承為前提設計:是否有文件說明,並且為擴充保留開放性?

如果三項都符合,使用繼承也沒問題。擴充 UIViewController 這類以框架為基礎的類別,就是代表性的案例。

相反地,只要有一項不明確,就先考慮組合。

我用表格簡單比較了一下。

情況 建議
純粹的 is-a 關係,且可以完全替代 繼承
只想單純重複使用程式碼時 組合
想在執行階段變更行為時 組合
需要組合多項功能時 組合

可以看到,實務上遇到的大多數情況都偏向組合。「favor composition」這個建議並不是沒有原因的。

放在表格中比較後,什麼時候該用哪一種一目了然
放在表格中比較後,什麼時候該用哪一種一目了然

我在實務上會這樣判斷

建立新類別時,我習慣先問自己:「這是父類別的一種,還是只是想借用父類別的功能?」

如果只是想借用功能,我幾乎總是選擇組合。

即使觀察設計模式,方向也很明確。策略模式、裝飾器模式等,全部都是以組合為基礎。

尤其是策略模式,它會將行為分離成物件,並在執行階段替換。這種彈性很難透過繼承模擬。

像替換積木一樣切換,正是組合的魅力
像替換積木一樣切換,正是組合的魅力

當然,你必須接受程式碼會稍微變長,因為需要自行撰寫委派方法。不過,之後要變更結構會方便得多。


「不要使用繼承」並不是禁止繼承,而是提醒你避免為了重複使用而濫用繼承。is-a 關係明確,就使用繼承;只是想借用功能,就使用組合。只要掌握這一個判準,程式碼就會穩固許多。祝你今天也能設計出好的架構!

推薦閱讀