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 CommonFindersMixin 也可以用來組合不同的驗證邏輯:
class AccountForCustomer extends Account
with AccountValidations, AccountCustomerValidations
class AccountForAdmin extends Account
with AccountValidations, AccountAdminValidationsTip 54 - Use Mixins to Share Functionality(使用 Mixin 來共享功能)
繼承很少是答案#
三種替代傳統類別繼承的方法:
| 替代方案 | 適用場景 |
|---|---|
| 介面和協定 | 用於共享型別資訊 |
| 委派 | 用於新增功能 |
| Mixin 和 trait | 用於共享方法 |
每一種在不同情況下可能更適合你,取決於你的目標是共享型別資訊、新增功能還是共享方法。請選擇最能表達你意圖的技術。
不要把整座叢林拖著走。
相關章節#
- Topic 8,好設計的本質
- Topic 10,正交性
- Topic 28,去耦合