2.2 Composition over Inheritance:組合優于繼承的實際應用
繼承(Inheritance)是面向對象編程中最容易被過度使用和誤用的特性之一。傳統的教學往往讓人們優先選擇繼承來實現代碼復用和建立“是一個(is-a)”的關系。然而,在復雜的業務系統中,嚴格的繼承層次結構常常會變得僵化、脆弱,最終難以維護。
“組合優于繼承”(Composition over Inheritance)是一條經典的設計原則,它倡導通過將對象組合在一起,而不是通過繼承來擴展功能。它旨在構建“有一個(has-a)”或“使用一個(uses-a)”的關系,從而獲得更大的靈活性。
2.2.1 繼承的陷阱:脆弱的基類問題
讓我們看一個典型的濫用繼承的例子:
// 一個看似合理的繼承層次結構
public abstract class OrderProcessorBase {public virtual void Validate(Order order) {// 基礎驗證邏輯}public virtual decimal CalculateTotal(Order order) {// 計算總價的基礎邏輯return order.Items.Sum(i => i.Price * i.Quantity);}public void Process(Order order) {Validate(order);order.Total = CalculateTotal(order);// ... 其他處理步驟,如保存、通知等}
}// 為特定場景擴展
public class OnlineOrderProcessor : OrderProcessorBase {public override void Validate(Order order) {base.Validate(order); // 先執行基礎驗證// 添加在線訂單特有的驗證,如配送地址必填if (string.IsNullOrEmpty(order.ShippingAddress))throw new InvalidOperationException("Shipping address is required.");}public override decimal CalculateTotal(Order order) {decimal baseTotal = base.CalculateTotal(order);// 添加在線訂單特有的費用,如運費return baseTotal + 5.0m;}
}public class TaxFreeOrderProcessor : OrderProcessorBase {public override decimal CalculateTotal(Order order) {// 免稅訂單,需要完全重寫計算邏輯,可能無法有效復用基類邏輯return order.Items.Sum(i => i.PriceBeforeTax * i.Quantity);}
}
這個設計的問題在于:
- 脆弱的基類(Fragile Base Class):對
OrderProcessorBase
的任何修改(例如,添加一個新的虛擬方法,修改Process
方法的流程)都可能會無聲地破壞所有子類的行為。子類與基類緊密耦合,基類變得“脆弱”。 - 爆炸性的子類:如果現在需要處理一種“在線且免稅”的訂單,你該怎么辦?創建
OnlineTaxFreeOrderProcessor
?這會導致類層次結構爆炸,產生復雜的菱形繼承問題(C#不支持多繼承,此路不通)。 - 不靈活的復用:
TaxFreeOrderProcessor
無法復用基類CalculateTotal
的邏輯,只能重寫,可能導致代碼重復。 - 單一演化方向:繼承強制子類沿著基類的單一維度進行演化。但現實世界的需求變化往往是多維度的(如支付方式、客戶等級、訂單來源等)。
2.2.2 組合的解決方案:策略模式與行為注入
讓我們用“組合”來重新設計上面的例子。我們將不同的行為(驗證、計算)抽取出來,定義為獨立的策略接口。
// 1. 定義抽象策略接口
public interface IOrderValidationStrategy {void Validate(Order order);
}
public interface IOrderCalculationStrategy {decimal CalculateTotal(Order order);
}
// ... 可以定義更多策略接口,如 INotificationStrategy, IPersistenceStrategy// 2. 實現各種具體策略
public class StandardValidationStrategy : IOrderValidationStrategy {public void Validate(Order order) { /* 基礎驗證邏輯 */ }
}
public class OnlineOrderValidationStrategy : IOrderValidationStrategy {public void Validate(Order order) {// 包含基礎驗證和在線特有驗證new StandardValidationStrategy().Validate(order);if (string.IsNullOrEmpty(order.ShippingAddress))throw new InvalidOperationException("Shipping address is required.");}
}
//---
public class StandardCalculationStrategy : IOrderCalculationStrategy {public decimal CalculateTotal(Order order) => order.Items.Sum(i => i.Price * i.Quantity);
}
public class OnlineCalculationStrategy : IOrderCalculationStrategy {public decimal CalculateTotal(Order order) => new StandardCalculationStrategy().CalculateTotal(order) + 5.0m;
}
public class TaxFreeCalculationStrategy : IOrderCalculationStrategy {public decimal CalculateTotal(Order order) => order.Items.Sum(i => i.PriceBeforeTax * i.Quantity);
}// 3. 核心訂單處理器,通過組合各種策略來完成工作
public class OrderProcessor {private readonly IOrderValidationStrategy _validationStrategy;private readonly IOrderCalculationStrategy _calculationStrategy;// ... 其他策略// 策略通過構造函數注入。這提供了極大的靈活性!public OrderProcessor(IOrderValidationStrategy validationStrategy,IOrderCalculationStrategy calculationStrategy){_validationStrategy = validationStrategy;_calculationStrategy = calculationStrategy;}public void Process(Order order) {_validationStrategy.Validate(order);order.Total = _calculationStrategy.CalculateTotal(order);// ... 使用其他策略}
}
現在,如何創建那個令人頭疼的“在線且免稅”訂單處理器?變得非常簡單:
// 在應用程序的 composition root (通常是DI容器配置處) 進行組裝
var processorForComplexOrder = new OrderProcessor(validationStrategy: new OnlineOrderValidationStrategy(), // 使用在線驗證calculationStrategy: new TaxFreeCalculationStrategy() // 使用免稅計算
);// 或者,你可以創建新的組合策略,而不是新的Processor類
public class OnlineTaxFreeCalculationStrategy : IOrderCalculationStrategy {public decimal CalculateTotal(Order order) {decimal baseTotal = new StandardCalculationStrategy().CalculateTotal(order);return baseTotal + 5.0m; // 在線費用,但免稅邏輯已融入?這里設計需要斟酌}
}
// 更好的方式是:認識到“加運費”和“免稅”是兩個正交的關切點,可能需要更細粒度的策略組合。
2.2.3 組合的優勢與架構師視角
- 靈活性(Flexibility):你可以動態地組合任何驗證策略和任何計算策略,輕松應對“在線且免稅”這種多維度的需求變化,而無需修改現有類或創建復雜的繼承樹。
- 解耦(Decoupling):
OrderProcessor
不再與任何具體的業務實現耦合,它只依賴于接口。各個策略之間也是相互獨立的。 - 可測試性(Testability):可以輕松地為
OrderProcessor
編寫單元測試,只需注入Mock的策略對象即可。 - 單一職責(Single Responsibility):每個策略類都只有一個非常明確的職責,符合SRP。
- 開閉原則(Open/Closed):添加新的驗證或計算方式(新的策略實現)完全不需要修改
OrderProcessor
或其他策略,符合OCP。
何時使用繼承?
“組合優于繼承”并非要完全否定繼承。繼承在以下場景仍然有效:
- 建立嚴格的類型層次結構:當存在真正的“is-a”關系,且子類確實是基類的一個更具體的類型時(如
Cat : Animal
,Button : Control
)。 - 需要多態:當你需要讓不同的子類對象對同一消息做出不同響應時。
- 框架設計:用于定義模板方法模式,其中基類控制主要流程,子類只負責實現特定的步驟。
決策指南:
- 默認優先選擇組合,尤其是對于行為的擴展。
- 詢問自己:“我是為了復用代碼,還是為了建立類型關系?”如果主要是為了復用,組合通常是更安全、更靈活的選擇。
- 如果使用繼承,確保子類和基類的關系是穩定且永久的,避免設計深度過大的繼承層次。
- 利用依賴注入框架來管理這些可組合的策略對象,這將使你的系統像由樂高積木組成一樣,可以輕松地拆卸和重組。
總結:
從“繼承思維”轉變為“組合思維”是邁向高級軟件設計的關鍵一步。它要求你從對象間的關系和行為的角度來思考,而不是僅從分類的角度。通過將系統分解為一系列精細、單一職責、可插拔的組件,并通過組合它們來構建復雜行為,你將創建一個真正靈活、健壯且易于演進的架構。這種思維方式是理解后續依賴注入(DI)和更高級架構模式的基礎。