學習軟體開發時,相信大家都曾因為 DI、IoC、DIP 這三個術語而傷過一次腦筋。
課程或文章常把三者塞進同一句話,例如「IoC 容器透過 DI 注入相依性,並遵守 DIP」,久而久之就容易搞不清楚各自代表什麼。
先說結論:三個術語位於不同層次。DIP 是原則,也就是為什麼要這麼做;IoC 是實現該原則的整體方向,也就是誰掌握控制權;DI 則是實作這個方向的具體技術,也就是如何注入。
讀完本文後,你會理解三者如何串聯,也能在面試被問到時流暢回答。我也盡量讓程式碼範例保持簡短。
先用一句話整理三個術語
先各記住一句話再開始,會容易理解許多。
- DIP(Dependency Inversion Principle,相依性反轉原則):不要依賴具體實作,而要依賴抽象(例如協定)的設計原則
- IoC(Inversion of Control,控制反轉):由框架或容器,而不是自己的程式碼,掌握程式的控制流程
- DI(Dependency Injection,相依性注入):不在內部建立所需物件,而是由外部傳入的技術
三者的關係如下。
如果 DIP 是目標與原則,IoC 就是實現該目標的廣義概念。DI 則是實作 IoC 的其中一種具體方法。
簡單來說,抽象層次會依 DIP > IoC > DI 的順序降低。
DIP 是「要依賴什麼」的問題
DIP 是 SOLID,也就是物件導向五大設計原則中的 D:相依性反轉原則。
核心有兩點:高層模組不應依賴低層模組,而且兩者都應依賴抽象。
聽起來有點難,但用程式碼看就簡單多了。
先看一個訂單服務直接綁定特定支付業者 Kakao Pay 的反面範例。
// 反面範例:直接依賴具體類別
class OrderService {
private let pay = KakaoPay() // 要更換支付業者,就得拆掉這裡
}
之後若要改用 Naver Pay,就必須直接修改 OrderService 的程式碼。每增加一種支付方式,高層模組就會受到影響。
DIP 會將這個方向反轉。定義支付這個抽象(協定),讓程式只依賴它。
protocol PayGateway { func pay(amount: Int) }
class OrderService {
private let pay: PayGateway // 依賴抽象,而不是具體類別
init(pay: PayGateway) { self.pay = pay }
}
無論是 Kakao Pay 還是 Naver Pay,只要實作 PayGateway,就不需要修改 OrderService。這就是「依賴方向反轉」的意思。
IoC 是「誰掌握控制權」的問題
IoC,也就是控制反轉,是更廣泛的概念。
一般來說,我們撰寫的程式碼會完全控制流程:建立物件、呼叫方法,並決定執行順序。
IoC 會反轉這個控制權。不是由我們呼叫框架,而是由框架呼叫我們。
它也常被稱為「好萊塢原則」:「不要打電話給我們,我們會打給你(Don’t call us, we’ll call you)。」
以 Swinject 這類 DI 容器為例,不是由我們直接建立物件,而是由容器代為建立、管理與連接物件。控制權便從我們轉移到容器。
這裡有一個重要重點:IoC 不只有 DI。
- 框架呼叫回呼
- 樣板方法模式
- UIKit 代為呼叫 viewDidLoad 等生命週期方法
這些全都是 IoC 的案例。DI 只是其中專門用來「注入相依性」的下位概念。
DI 是「如何注入」的問題
DI,也就是相依性注入,是實作 IoC 最具代表性的技術。
在剛才的 DIP 範例中,OrderService 透過初始化器接收 PayGateway,這就是 DI。不在內部直接建立所需物件,而是從外部傳入。
注入方式大致分為三種。
| 注入方式 | 說明 | 推薦程度 |
|---|---|---|
| 建構子注入 | 透過 init 接收相依性 | 最推薦(不可變、必要相依性明確) |
| 方法注入 | 透過方法參數接收 | 適用於選用的相依性 |
| 屬性注入 | 建立後指定給屬性 | 會變成 optional·var,因此不推薦 |
Swift 建議使用初始化器注入。相依性可以用 let 固定,較為安全;測試時也容易傳入假物件(Mock)。
整理起來,關係如下。
為了遵守 DIP 原則,沿著 IoC 的方向移交控制權,並以 DI 作為具體手段。
這句話最精簡地概括了三個術語的關係。
常見問題整理
Q. DI 和 IoC 不是同一件事嗎?
不是。IoC 是更廣泛的概念,DI 是實作 IoC 的多種方法之一。所有 DI 都屬於 IoC,但不是所有 IoC 都是 DI。
Q. 遵守 DIP 就會自動使用 DI 嗎?
不一定。DIP 是「依賴抽象」的原則,而從外部傳入該抽象的實作時,才構成 DI。原則與技術是兩回事。
Q. 不使用 Swinject 這類函式庫,也能使用 DI 嗎?
可以。像上面的 Swift 範例一樣,透過初始化器直接傳入也是 DI。Swinject 或 Factory 只是將流程自動化的工具,DI 本身不需要函式庫也能成立。
總結一下:DIP 是為什麼,也就是原則;IoC 是誰,也就是控制權;DI 是如何,也就是技術。用這三個問題分開記憶,就不會再混淆。
了解三者位於不同層次後,DI 相關程式碼看起來會完全不同。希望這篇文章能讓你豁然開朗地整理好概念。加油!

