You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.

— Joe Armstrong

核心概念#

你是否用物件導向語言程式設計?你是否使用繼承?

如果是的話,停下來吧! 這可能不是你想做的事。

一些背景#

繼承最初出現在 1969 年的 Simula 67,是一種優雅的解決方案——使用 prefix classes 來處理在同一列表中排隊不同類型事件的問題。Simula 的方法是將繼承視為組合類型的方式。

Smalltalk 的 Alan Kay 則把繼承視為行為的動態組織——純粹為了行為而子類化(「差異式程式設計」)。

這兩種風格在接下來的數十年中發展:

  • Simula 方式(繼承作為組合類型):繼續在 C++ 和 Java 等語言中
  • Smalltalk 方式(繼承作為行為的動態組織):見於 Ruby 和 JavaScript 等語言

現在我們面對的是一整代 OO 開發者使用繼承的兩個原因:他們不想打字他們喜歡型別

不幸的是,兩種繼承都有問題。

使用繼承共享程式碼的問題#

繼承就是耦合。不僅子類耦合到父類、父類的父類等等,使用子類的程式碼也耦合到所有祖先。

class Vehicle
  def initialize
    @speed = 0
  end
  def stop
    @speed = 0
  end
  def move_at(speed)
    @speed = speed
  end
end

class Car < Vehicle
  def info
    "I'm car driving at #{@speed}"
  end
end

當 Vehicle 的開發者將 move_at 改名為 set_velocity、將 @speed 改為 @velocity 時——Car 類別(甚至不是 Vehicle 的直接使用者)也會無聲地壞掉。

使用繼承建構型別的問題#

有些人把繼承視為定義新型別的方式,他們最愛的設計圖是類別階層圖。不幸的是,這些圖很快會長成龐大的怪物——一層又一層,只為了表達類別之間最微小的差異。這種複雜度讓應用程式更脆弱,因為變更會在許多層之間波動。

更糟的是多重繼承問題。一個 Car 可能是 Vehicle,但也可能是 Asset、InsuredItem、LoanCollateral 等。正確建模需要多重繼承,但許多現代 OO 語言不提供它。

Tip 51 - Don’t Pay Inheritance Tax(不要付繼承稅)

替代方案更好#

作者建議三種技術,意味著你永遠不需要再使用繼承:

1. 介面與協定(Interfaces and Protocols)#

大多數 OO 語言讓你指定一個類別實作一個或多個行為集。例如在 Java 中:

public class Car implements Drivable, Locatable {
  // 必須實作 Drivable 和 Locatable 的方法
}

介面和協定的強大之處在於我們可以將它們用作型別。如果 Car 和 Phone 都實作了 Locatable,我們可以將兩者存放在同一個可定位項目的列表中。

Tip 52 - Prefer Interfaces to Express Polymorphism(偏好使用介面來表達多型)

2. 委派(Delegation)#

繼承鼓勵開發者建立具有大量方法的類別。如果父類有 20 個方法,子類只想用其中 2 個,其物件仍然會帶著另外 18 個。

替代方案是使用委派:不再繼承框架的基底類別,而是持有一個被委派者的參考:

class Account
  def initialize(...)
    @repo = Persister.for(self)
  end
  def save
    @repo.save()
  end
end

現在我們不暴露框架 API 的任何部分給 Account 類別的客戶端。

Tip 53 - Delegate to Services: Has-A Trumps Is-A(委派給服務:Has-A 勝過 Is-A)

3. Mixin 與 Trait#

Mixin 的基本想法很簡單:我們想要在不使用繼承的情況下,擴充類別和物件的新功能。建立一組函式、給它一個名稱、然後以某種方式擴充類別或物件。

mixin CommonFinders {
  def find(id) { ... }
  def findAll() { ... }
}
class AccountRecord extends BasicRecord with CommonFinders
class OrderRecord   extends BasicRecord with CommonFinders

Mixin 也可以用來組合不同的驗證邏輯:

class AccountForCustomer extends Account
    with AccountValidations, AccountCustomerValidations

class AccountForAdmin extends Account
    with AccountValidations, AccountAdminValidations

Tip 54 - Use Mixins to Share Functionality(使用 Mixin 來共享功能)

繼承很少是答案#

三種替代傳統類別繼承的方法:

替代方案適用場景
介面和協定用於共享型別資訊
委派用於新增功能
Mixin 和 trait用於共享方法

每一種在不同情況下可能更適合你,取決於你的目標是共享型別資訊、新增功能還是共享方法。請選擇最能表達你意圖的技術。

不要把整座叢林拖著走。

相關章節#

  • Topic 8,好設計的本質
  • Topic 10,正交性
  • Topic 28,去耦合