本章介紹四種常見的 DI 反模式。這些做法表面上看似合理甚至有用,但實際上會破壞鬆耦合的目標,應當辨識並避免。

Control Freak#

Control Freak 是最常見的 DI 反模式——在 Composition Root 以外的地方直接依賴 Volatile Dependency。

常見表現形式:

  • 直接 new 出 Volatile Dependency 的具體實作
  • Factory 內部硬編碼具體型別
  • 提供多載建構子 (overloaded constructor),在無參數版本中建立預設依賴
// Control Freak: 在業務邏輯中直接建立具體依賴
public class ProductService
{
    private readonly SqlProductRepository repository;

    public ProductService()
    {
        this.repository = new SqlProductRepository(); // 緊耦合
    }
}

new 關鍵字本身不是問題。對 Stable Dependencies(如 stringList<T>、DTO 等)使用 new 完全正常。只有在 Composition Root 以外對 Volatile Dependencies 使用 new 才是 Code Smell。

Figure 5.2: 靜態 ProductRepositoryFactory 造成 Domain 與 Data Access 層之間的循環依賴

修正方式:將依賴透過 Constructor Injection 注入,並將組裝邏輯移至 Composition Root。

Service Locator#

Service Locator 是最危險的反模式,因為它偽裝成一種解決方案

核心問題:

  • 隱藏依賴——類別的 constructor 不再誠實宣告它需要什麼
  • 將編譯期錯誤推遲到執行期——缺少註冊只會在 runtime 爆炸
  • 將 Container 依賴拖進整個程式碼庫——每個類別都必須知道 Service Locator 的存在
// Service Locator: 主動向 Container 要依賴
public class ProductService
{
    public void GetFeaturedProducts()
    {
        var repository = Locator.GetService<IProductRepository>();
        // ...
    }
}

Figure 5.3: HomeController 與 Service Locator 之間的互動

DI Container 在 Composition Root 以外被使用,就是 Service Locator。 這是判斷的核心準則。即使是「官方的」DI Container,錯誤的使用方式仍然是反模式。

Figure 5.5: Visual Studio IntelliSense 只能告訴我們 ProductService 有無參數建構子,其依賴完全不可見

修正方式:以 Constructor Injection 取代,讓依賴在 constructor 中明確宣告。

Ambient Context#

Ambient Context 透過 static accessor 提供單一 Dependency,讓整個應用程式都能「環境式地」存取。

常見範例:

  • TimeProvider.CurrentDateTime.Now 的靜態包裝
  • 靜態 Logger(如 Log.Information(...)
  • Thread.CurrentPrincipal

問題分析:

  • 隱藏依賴——與 Service Locator 相同,無法從 constructor 看出依賴關係
  • 難以測試——必須操作全域狀態,平行測試會互相干擾
  • 違反 SRP——任何類別都能輕易存取,鼓勵職責擴散
  • 共享可變狀態——多執行緒環境下容易產生競態條件 (race condition)

修正方式:將原本透過 static accessor 取得的依賴改為 Constructor Injection,讓依賴關係顯式化。

Constrained Construction#

Constrained Construction 發生在程式碼假設所有實作都必須具備特定的 constructor 簽章(通常是無參數建構子),以便透過反射 (reflection) 進行 late binding。

// Constrained Construction: 假設所有 plugin 都有無參數建構子
Type type = Type.GetType(typeName);
var plugin = (IPlugin)Activator.CreateInstance(type);

問題:

  • 限制了實作類別的彈性——無法透過 constructor 注入自身的依賴
  • 阻礙了正規的 DI 運作方式
  • 實質上是一種隱式的介面合約,但編譯器無法檢查

修正方式:使用 DI Container 或在 Composition Root 中以 Constructor Injection 的方式建立物件,不再限制 constructor 簽章。

反模式辨識摘要#

四種反模式的快速比較
反模式核心問題典型信號修正方向
Control FreakComposition Root 外建立 Volatile Dependencynew ConcreteType() 散布各處Constructor Injection
Service Locator主動向 Container 索取依賴container.GetService<T>() 出現在業務程式碼Constructor Injection
Ambient Context透過 static accessor 存取依賴SomeContext.Current、靜態 LoggerConstructor Injection
Constrained Construction假設固定 constructor 簽章Activator.CreateInstance(type)DI Container / Composition Root