目標:撰寫可維護的程式碼#

Dependency Injection 的終極目標不是引入花俏的技術,而是撰寫可維護的程式碼 (Maintainable Code)。DI 是達成鬆耦合 (Loose Coupling) 的一組技術與原則,而鬆耦合正是讓軟體在長期演進中保持可維護性的關鍵。

破除四個常見迷思#

許多開發者對 DI 存在誤解,書中逐一破除:

  1. 「DI 只用於 Late Binding」——錯。Late Binding(例如透過設定檔切換實作)只是 DI 的好處之一,並非唯一目的。
  2. 「DI 只用於 Unit Testing」——錯。可測試性是重要好處,但 DI 帶來的價值遠不止於此。
  3. 「DI 是一種強化版的 Abstract Factory」——錯。DI 是一種設計模式與原則的集合,不是一個特定的 Factory 模式。
  4. 「DI 需要 DI Container」——錯。完全可以用 Pure DI(手動組裝物件圖)來實踐 DI,不依賴任何框架。

DI Container 是可選的工具,不是 DI 的必要條件。本書前三部分完全使用 Pure DI,第四部分才介紹 DI Container。

電線插座比喻#

書中用日常生活中的電線插座系統來類比 DI 的設計原則:

  • 插頭與插座 = Liskov Substitution Principle (LSP):只要符合介面規格,任何裝置都能插上使用

Figure 1.4: 透過插座與插頭,吹風機可以與牆壁插座鬆耦合

Figure 1.5: 使用插座與插頭,可以將吹風機替換為電腦——對應 Liskov Substitution Principle

  • 安全插座 (Safety Plug) = Null Object Pattern:提供一個「什麼都不做」的預設實作
  • UPS 不斷電系統 = Decorator Pattern:在不改變原有行為的前提下,包裹額外功能

Figure 1.7: 加入 UPS 不斷電系統以防斷電——對應 Decorator 設計模式

  • 電源延長線 (Power Strip) = Composite Pattern:將多個實作組合為一個

Figure 1.8: 延長線讓多個電器共用一個插座——對應 Composite 設計模式

  • 轉接頭 (Adapter) = Adapter Pattern:讓不相容的介面得以合作

Hello DI! 範例#

書中以一個最簡範例說明 DI 的基本結構:

public class Salutation
{
    private readonly IMessageWriter writer;

    public Salutation(IMessageWriter writer)
    {
        this.writer = writer;
    }

    public void Exclaim()
    {
        writer.Write("Hello DI!");
    }
}
  • IMessageWriterAbstraction(介面)
  • ConsoleMessageWriter 是具體實作,將訊息寫到 Console
  • Salutation 透過 Constructor Injection 接收依賴,完全不知道具體實作是什麼

Figure 1.10: Hello DI! 應用程式中各協作者的關係

DI 的五大好處#

好處說明
Late Binding不需重新編譯即可替換元件實作
Extensibility透過新增程式碼(而非修改)來擴充功能
Parallel Development團隊成員可以針對介面各自開發
Maintainability類別職責單一,容易理解與修改
Testability可注入 Test Double 進行隔離測試

Stable vs Volatile Dependencies#

區分依賴類型是決定是否需要注入的關鍵判斷:

Stable Dependencies#

符合以下條件的依賴屬於穩定依賴,通常不需要透過 DI 注入:

  • 已經可用(如 .NET BCL 中的類別)
  • 行為確定性高 (Deterministic)
  • 不需要特別的環境設定
  • 不會有需要替換的情境

Volatile Dependencies#

符合以下任一條件即為揮發性依賴,應該透過 DI 注入:

  • 仍在開發中,尚未穩定
  • 不是在所有環境都可用(如資料庫、外部服務)
  • 行為不確定 (Nondeterministic),如時間、亂數
  • 未來需要替換或包裹額外行為

判斷是否該注入一個依賴時,問自己:「這個依賴在測試環境或其他部署環境中會不會需要替換?」如果答案是肯定的,它就是 Volatile Dependency。

DI 的三個維度#

DI 涵蓋三個核心維度,每個維度處理不同的關注點:

  1. Object Composition——如何組裝物件圖 (Object Graph)?哪個物件依賴哪個物件?
  2. Object Lifetime——每個依賴的生命週期為何?Singleton? Transient? Scoped?
  3. Interception——如何在不修改原有程式碼的情況下,攔截並擴充行為?

Figure 1.12: 攔截 ConsoleMessageWriter

關鍵定義#

Pure DI

不使用任何 DI Container,完全以手動方式在 Composition Root 中組裝物件圖的做法。這是理解 DI 最直接的方式。

Constructor Injection

透過建構函式的參數來宣告並接收依賴。這是最常見、最推薦的 DI 模式。

Module

一個可部署的邏輯單元(通常對應一個 Assembly 或 Package),用來組織相關的類別與介面。DI 的設計很大程度取決於 Module 之間的依賴方向。