Software Design - Principle - Inversion of Control (IoC)

Quick Chat

「控制反轉(Inversion of Control, IoC)」這個詞常常讓人一頭霧水──到底反轉了什麼?

更麻煩的是,它還很容易和「依賴倒置(Dependency Inversion)」混淆 😅

如果你在開發時有出現以下這些疑惑,那麼你其實已經在思考 IoC 要解決的問題了:

  1. 為什麼到處都要 new
  2. 建構子的參數要怎麼安排?初始化邏輯怎麼整理?
  3. 物件能不能被共享?生命週期誰來管理?何時該釋放?
  4. 是否需要一個「專門管理依賴」的角色?
  5. 這個類別既要處理業務邏輯,又要負責 new 物件,職責是不是太混亂了?

IoC 正是為了解決這些問題而提出的,它帶來一個核心理念:

依賴的「使用者」不再自己主動去建立和配置依賴,而是把這個「控制權」交給外部機制(通常是 IoC 容器)。

換句話說:

依賴的「使用者」只需要「接收」或「查詢」它所需的依賴,然後專心「使用」它們。這樣它就能專注在核心邏輯上,而不必分心處理依賴管理,進而提升模組化與可維護性。

Advantages

  • 降低耦合度: 需求方與具體實現之間不再直接關聯,每個模組都可以獨立開發、測試與替換,互不影響。

  • 集中管理依賴: 容器統一管理所有物件的建立和生命週期。當您需要替換某個服務的實作時,只需要修改容器的配置,而不需要動到多處程式碼。

  • 避免重複建構: 容器可以管理共享的物件實例,有效避免重複建立,提高資源利用率。

  • 提升測試便利性: 透過 IoC 容器,在進行單元測試時,可以輕鬆地將真實的服務替換為模擬物件(mock)測試替身(stub)

Practice

實現 IoC 有兩種常見方式:依賴注入(Dependency Injection, DI)依賴尋找(Dependency Lookup)

依賴注入(DI)

這是目前最主流且推薦的實踐方式,核心概念是由容器「被動地」將依賴項傳遞給需求方

  • 實作原理: 容器會主動將所需的依賴(如服務物件)透過以下方式注入到您的類別中:

    • 建構子注入(Constructor Injection): 在物件建構時,透過建構子的參數傳入依賴。這是最推薦的方式,因為它可以確保物件在建立時就擁有所有必要的依賴,讓依賴關係更清晰。

    • 屬性注入(Property Injection): 透過公開的屬性(Setter)來傳入依賴。

    • 方法注入(Method Injection): 透過特定的方法來傳入依賴。

  • 優點: 這種方式讓您的程式碼無需知道容器的存在(理想狀態),因為依賴是「被動」傳入的,大大降低了耦合度。

依賴尋找(Dependency Lookup)

這種方式的核心是由需求方「主動地」向容器請求所需的依賴

  • 實作原理: 需求方會直接呼叫容器的方法(例如 container.resolve()),來取得所需的服務。

  • 優點: 簡單直觀。

  • 缺點: 這種方式存在爭議,被視為一種反模式(anti-pattern),因為它讓需求方直接與容器耦合,失去了 IoC 應有的解耦優勢。最典型的實作就是 服務定位器(Service Locator)

Trade-off

儘管 IoC 容器提供了強大的功能,但它也需要權衡:

  • 框架依賴性: 大多數 IoC 容器需要依賴特定的框架來管理依賴關係,這會增加專案的複雜性。

  • 學習曲線: 導入 IoC 容器框架通常需要額外的學習成本,特別是對於小型或簡單的專案而言,可能過於複雜。

組合根(Composition Root):「窮人的 DI」

組合根指的是應用程式中集中管理所有依賴的建立與組裝的地方,通常位於應用程式的進入點(如 main 方法或 Web 應用程式的啟動類別)。這裡就是您定義依賴注入規則並初始化所有服務的地方。

在許多簡單的專案或情境下,您不需要引入龐大的框架。只需要在組合根手動建構和組裝所有依賴,就能實現 DI 的核心精神。這種做法就是常說的**「窮人的 DI」**,它依然能有效解耦,同時避免了框架帶來的額外負擔。