我們常會在程式碼中需要物件的地方直接建立並使用它。但一寫測試就會碰壁,因為類別內寫死的建立程式碼,讓物件幾乎無法替換。
搜尋相依性注入(DI)時,常會看到「控制反轉」或「IoC 容器」等說法,反而更容易感到困惑。
所以今天就用最簡單的方式說明。先講結論。
相依性注入(DI)就是在類別外建立原本在類別內直接建立的物件,再傳入類別。關鍵就在這個移動:把物件建立的位置移到類別外。這幾乎就是DI的全部。
讀完這篇文章,你會了解為什麼需要DI、程式碼如何改變,以及為什麼Swinject這類DI函式庫能替你處理這件事。
如果物件是在類別內建立,問題在哪裡?
假設有一個處理訂單的類別。這個類別需要付款模組來處理付款。
最常見的新手程式碼會長這樣。
class OrderService {
// 在類別內直接建立付款物件
private let pay = KakaoPay()
func order() { pay.pay() }
}
問題在於,OrderService會緊密耦合到KakaoPay。
如果想把KakaoPay換成Naver Pay呢?就得進入類別修改建立的部分。
即使測試時想放入假的付款物件,也沒有辦法,因為類別已經在內部建立了真正的KakaoPay。
也就是說,把物件建立放在類別內,會讓類別被特定實作綁住。
所以我們把物件建立移到外部
解法比想像中簡單:不要自己建立,只要在外部建立後接收即可。
class OrderService {
private let pay: Pay
// 只接收外部建立並傳入的物件
init(pay: Pay) { self.pay = pay }
func order() { pay.pay() }
}
改變的只有一件事。直接建立消失了,改成透過初始化器接收物件。
現在OrderService不需要知道付款是KakaoPay還是Naver Pay,只要知道「接收並使用名為Pay的東西」即可。
DI的本質並不複雜,只是把物件建立移到類別外,將決定要傳入什麼的權限交給外部。
從外部放入物件稱為「注入(injection)」。注入相依的物件,所以叫作相依性注入。
只是名稱比較難,實際做的事就是剛才看到的全部。
使用DI有什麼好處?
只用口頭說很好可能不容易有感,因此整理了實際的變化。
- 更容易替換:把KakaoPay換成Naver Pay時,OrderService一行都不用改,只要替換傳入的物件即可。
- 測試更方便:放入假的(Mock)付款物件取代真正付款,就能不花錢只驗證邏輯。
- 職責更清楚:OrderService只專注於「處理訂單」,「要使用哪種付款」則由外部決定。
我覺得第三點最有感。類別只專注自己的工作後,程式碼真的好讀很多。
總結如下。
| 分類 | 在內部建立 | 移到外部建立(DI) |
|---|---|---|
| 替換付款方式 | 需要修改類別 | 只替換傳入的物件 |
| 測試 | 無法放入假物件 | 可以注入Mock |
| 關注點 | 建立與邏輯混在一起 | 只專注於邏輯 |
那麼,為什麼需要Swinject這類DI函式庫?
到了這裡,自然會產生疑問:「知道要從外部傳入了,但這個『外部』由誰管理?」
物件不多時,可以手動建立並傳入。但實務專案可能有數百個彼此牽連的物件。
要由人逐一按照順序建立並傳入,簡直是地獄。
因此,出現了代替我們處理這項「從外部建立並傳入」工作的工具。Swift陣營的Swinject或Factory等DI容器,就是負責這個角色。
先註冊需要的物件,容器就會自動建立並放入需要的位置。自己建立物件的工作幾乎消失了。
常說的「控制反轉(IoC)」也是從這裡來的,意思是建立並連接物件的控制權,從自己的程式碼移交給了容器。
簡單來說,DI是概念,而DI容器則是代替你執行這個概念的工作者。
常見問題
Q. DI和IoC是同一件事嗎? 不是。IoC(控制反轉)是「移交控制權」的廣泛原則,而DI是實現它的具體方法之一。可以把DI視為IoC的一種。
Q. 除了建構子,還有其他注入方式嗎? 有。包括建構子注入、屬性注入和方法注入。不過Swift建議使用建構子注入,因為相依性可用let固定,不會改變,也更容易測試。
Q. 使用DI一定需要函式庫嗎? 不需要。剛才手動放入建構子的做法也是正統的DI。函式庫只是將這件事自動化。
今天我們把相依性注入解釋成一個很小的移動:移動物件建立的位置。
如果覺得DI很難,只要記住一句話:「不要在類別內建立,改在外部建立後傳入。」記住這句,其他內容就會自然理解。
如果你曾在測試程式碼前感到不知所措,建議親手改寫一次今天的範例。會比只用眼睛看更確實地掌握。

