本章探討三種與 DI 相關的程式碼異味。關鍵觀念是:這些異味通常不是 DI 造成的問題,而是 DI(特別是 Constructor Injection)讓底層的設計缺陷變得可見。正確的應對方式是改善設計,而非繞過 DI。

Constructor Over-injection#

當一個類別的 constructor 參數超過四個,通常代表這個類別承擔了太多職責,違反了 Single Responsibility Principle (SRP)

Constructor Injection 只是讓 SRP 違規「浮出水面」。不要怪罪 DI——問題在於類別設計本身。

Figure 6.1: OrderService 有五個直接依賴,暗示違反 SRP

錯誤的修正方式#

將部分參數改為 Property Injection——這只是隱藏症狀,依賴關係並沒有減少,反而失去了 Constructor Injection 提供的編譯期保障。

正確的修正方式#

Facade Services#

建立新的 Abstraction,將多個相關的 Dependencies 封裝在一起,同時封裝它們的互動行為。

// 重構前:OrderService 有太多依賴
public OrderService(
    IOrderRepository orderRepo,
    IInventoryService inventory,
    IPaymentService payment,
    INotificationService notification,
    IShippingService shipping) { ... }

// 重構後:引入 Facade Service
public OrderService(
    IOrderRepository orderRepo,
    IOrderFulfillmentService fulfillment) { ... }

// OrderFulfillmentService 封裝了 inventory + payment + shipping 的互動

Facade Service 不是簡單的 Parameter Object——它必須封裝行為,不只是打包參數。如果新的 Abstraction 只是把依賴收集在一起而沒有自己的邏輯,那就是 Header Interface 反模式。

Figure 6.2: 兩個 OrderService 依賴聚合在 Facade Service 背後

Domain Events#

Domain Events 捕捉觸發狀態變更的動作,將核心邏輯與 Cross-Cutting Concerns 解耦。

  • 核心操作只負責發布事件
  • 事件處理器 (Event Handlers) 各自負責通知、日誌、稽核等關注點
  • 減少核心類別對多個 Cross-Cutting 服務的直接依賴

Figure 6.3: 重構後的最終 OrderService 依賴結構

Abuse of Abstract Factories#

Abstract Factory 在 DI 中經常被過度使用,但大多數情況下是不必要的。

常見的濫用場景#

生命週期不匹配問題:長壽命的消費者需要短壽命的依賴,因此引入 Factory 來「按需建立」。

// 常見但通常不必要的 Factory 用法
public class OrderController
{
    private readonly IOrderServiceFactory factory;

    public void PlaceOrder(OrderDto dto)
    {
        var service = factory.Create(); // 每次建立新實例
        service.Process(dto);
    }
}

這類問題通常可以透過正確配置 DI Container 的 Lifestyle (Scoped、Transient 等) 來解決,不需要 Factory。

執行期資料選擇:根據 runtime 條件選擇不同實作——更好的做法是使用 Composite PatternStrategy Pattern

判斷準則:無參數的 Create() 方法幾乎總是 Code Smell。如果 Factory 不需要任何執行期參數就能建立物件,那這個 Factory 很可能是多餘的。

合理使用的條件#

只有在需要傳入真正的執行期資料作為參數來建立物件時,Abstract Factory 才是合理的選擇。

Cyclic Dependencies#

Cyclic Dependencies(循環依賴)通常是 SRP 違規的信號——兩個或多個類別互相依賴,代表它們的職責邊界劃分不當。

Figure 6.12: Chicken 與 Egg 之間的依賴循環

解決策略#

依優先順序:

  1. 拆分類別——找出混合在一起的職責,將其分離為獨立的類別,打破循環
  2. 引入中介者 (Mediator) 或 Domain Events——讓雙方不直接依賴彼此,而是透過中介機制溝通
  3. Property Injection——作為最後手段,用於打破其中一個方向的依賴

Property Injection 只是技術上的解法,並未解決根本的設計問題。應優先考慮前兩種策略。

Figure 6.14: 將 IUserRepository 分離為兩個介面以打破依賴循環

程式碼異味處理摘要#

三種 Code Smells 的識別與修正
Code Smell識別信號根本原因正確修正
Constructor Over-injectionConstructor 參數 > 4違反 SRPFacade Services / Domain Events
Abuse of Abstract Factories無參數 Create() 方法不必要的間接層正確配置 Lifestyle / Composite Pattern
Cyclic DependenciesA 依賴 B 且 B 依賴 ASRP 違規拆分類別 / Mediator / Domain Events