軟體設計

什麼是相依性注入(DI)?從把 new 移到類別外開始(範例與比喻總整理)

我們常會在程式碼中需要物件的地方直接建立並使用它。但一寫測試就會碰壁,因為類別內寫死的建立程式碼,讓物件幾乎無法替換。

閱讀 4 分鐘
什麼是相依性注入(DI)?從把 new 移到類別外開始(範例與比喻總整理) 封面圖

我們常會在程式碼中需要物件的地方直接建立並使用它。但一寫測試就會碰壁,因為類別內寫死的建立程式碼,讓物件幾乎無法替換。

搜尋相依性注入(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
關注點 建立與邏輯混在一起 只專注於邏輯
把new移到外部後,程式碼會變得如此簡潔
把new移到外部後,程式碼會變得如此簡潔

那麼,為什麼需要Swinject這類DI函式庫?

到了這裡,自然會產生疑問:「知道要從外部傳入了,但這個『外部』由誰管理?」

物件不多時,可以手動建立並傳入。但實務專案可能有數百個彼此牽連的物件。

要由人逐一按照順序建立並傳入,簡直是地獄。

因此,出現了代替我們處理這項「從外部建立並傳入」工作的工具。Swift陣營的Swinject或Factory等DI容器,就是負責這個角色。

由容器建立,再插入需要的位置
由容器建立,再插入需要的位置
物件多到數百個時,就會感激有容器替你管理這些物件
物件多到數百個時,就會感激有容器替你管理這些物件

先註冊需要的物件,容器就會自動建立並放入需要的位置。自己建立物件的工作幾乎消失了。

常說的「控制反轉(IoC)」也是從這裡來的,意思是建立並連接物件的控制權,從自己的程式碼移交給了容器。

簡單來說,DI是概念,而DI容器則是代替你執行這個概念的工作者。


常見問題

Q. DI和IoC是同一件事嗎? 不是。IoC(控制反轉)是「移交控制權」的廣泛原則,而DI是實現它的具體方法之一。可以把DI視為IoC的一種。

Q. 除了建構子,還有其他注入方式嗎? 有。包括建構子注入、屬性注入和方法注入。不過Swift建議使用建構子注入,因為相依性可用let固定,不會改變,也更容易測試。

Q. 使用DI一定需要函式庫嗎? 不需要。剛才手動放入建構子的做法也是正統的DI。函式庫只是將這件事自動化。


今天我們把相依性注入解釋成一個很小的移動:移動物件建立的位置。

如果覺得DI很難,只要記住一句話:「不要在類別內建立,改在外部建立後傳入。」記住這句,其他內容就會自然理解。

如果你曾在測試程式碼前感到不知所措,建議親手改寫一次今天的範例。會比只用眼睛看更確實地掌握。

延伸閱讀