本章介紹四種核心的 DI 模式,並提供決策樹幫助開發者在不同情境下選擇正確的注入方式。

Composition Root#

Composition Root 是應用程式中組裝所有模組的唯一邏輯位置,盡可能靠近應用程式的進入點 (entry point)。

  • 它不是一個方法或一個類別,而是一個概念——可能跨越多個類別與方法
  • 每個應用程式應有且僅有一個 Composition Root
  • 如果使用 DI Container,Composition Root 是唯一可以參照 Container 的地方

Figure 4.1: Composition Root 負責組裝鬆耦合類別的物件圖,直接依賴系統中所有模組

在 Composition Root 以外的地方使用 DI Container,就變成了 Service Locator 反模式(詳見第五章)。

在 Composition Root 中看到大量的依賴組裝程式碼是完全正常的,這種「Apparent Dependency Explosion」只是將原本分散隱藏在各處的耦合關係集中顯現,屬於基礎設施層級的關注點。

Figure 4.3: Mary 應用程式與鬆耦合應用程式的依賴圖比較

Constructor Injection#

Constructor Injection 是預設首選的注入方式,用於靜態宣告類別的必要依賴 (Required Dependencies)

實作要點:

  • 透過 constructor 參數接收 Dependencies
  • 儲存於 private readonly 欄位
  • 使用 Guard Clauses 防止 null
public class ProductService
{
    private readonly IProductRepository repository;
    private readonly IUserContext userContext;

    public ProductService(
        IProductRepository repository,
        IUserContext userContext)
    {
        this.repository = repository
            ?? throw new ArgumentNullException(nameof(repository));
        this.userContext = userContext
            ?? throw new ArgumentNullException(nameof(userContext));
    }
}

Figure 4.5: 使用 Constructor Injection 建構 HomeController 實例

Constructor Injection 保證物件在建構完成後永遠不會處於不一致的狀態,因為所有必要依賴在建構時就已確認存在。

Method Injection#

Method Injection 適用於以下兩種情境:

  • Dependency 隨每次呼叫而變化——呼叫端決定提供哪個實作
  • 需要將 Dependency 注入以資料為中心的物件 (data-centric objects)——如 Domain Entity
public decimal ApplyDiscountFor(IUserContext userContext)
{
    // 根據使用者身份計算折扣
    bool preferred = userContext.IsInRole(Role.PreferredCustomer);
    return preferred ? this.Price * 0.95m : this.Price;
}

Method Injection 將依賴的選擇權交給呼叫端,而非在建構時就固定下來。

Property Injection#

Property Injection 的使用條件最為嚴格,僅適用於同時滿足以下兩個條件的情境:

  • 存在一個良好的 Local Default(預設實作位於同一個 assembly,保證一定可用)
  • Dependency 是真正可選的,使用預設值就能正常運作

Property Injection 不應該被用來替代 Constructor Injection。常見的錯誤是為了避免 Constructor Over-injection 而改用 Property Injection,這只是隱藏了問題(詳見第六章)。

主要使用場景是可重用函式庫 (reusable libraries) 中的擴充點 (extensibility model),讓使用者可以選擇性地替換預設行為。

決策樹#

選擇注入方式時,依序考慮:

  1. Dependency 是否為必要的? → 使用 Constructor Injection(大多數情況)
  2. Dependency 是否隨每次呼叫而變化? → 使用 Method Injection
  3. 是否有良好的 Local Default 且 Dependency 真正可選? → 使用 Property Injection

Figure 4.9: 模式決策流程——大多數情況應選擇 Constructor Injection

各模式的適用場景對照
模式適用時機典型範例
Composition Root應用程式啟動時組裝所有模組Startup.csMain method
Constructor Injection宣告必要依賴(預設選擇)Service 類別的 Repository 依賴
Method Injection依賴隨呼叫變化或注入 EntityApplyDiscountFor(IUserContext)
Property Injection有 Local Default 的可選依賴框架的 Logger、Formatter 擴充點