Dependency Injection,不是控制,而是放手

森林的深處,有一間小木屋。每當霧氣升起,樹葉發出細碎的聲響,動物們會緩緩走進這裡
一隻老貓躺在椅上,望著壁爐裡跳動的火光,輕輕說:「我們啊,無法事事都靠自己。」
這裡的每一份溫暖,都不是自己製造的,而是來自彼此的信任與協作,柴火是兔子帶來的,窗簾是松鼠縫的,茶是隔壁小鹿泡的
就如同我們不在類別中自行建構一切,也不強求自己了解每個細節,我們只需要設計出一個能被好好照顧的服務,然後放手交給外界,注入我們所需的一切
🪵 依賴功能性、而非實體

小時候為了玩遊戲、上網找攻略,寬頻連不上網路時,打中華電信打到哭
直到 ISP 的人被我煩到受不了終於親自出馬,他換了個小烏龜、在電腦上喬一喬設定就好了,你再次連上了網路,像是再次擁有了全世界
而這招就是所謂的 Dependency Injection,我不去了解網路的細節,不懂自己實作與測試,只靠著 哀求注入法 將外部服務注入進來解決我那空虛的童年
這裡與工作人員之間的互動,是依賴「抽象(角色)」,而不是「具體(人名)」
我並不在乎中華電信派來的人是誰,我只關心是否有實作 IInternetProblemHandler 的實體來幫你處理你手上的問題,誰來都行,而這就是所謂的依賴抽象(依賴介面),而不是綁定具體對象,所謂的介面,就是一個訂製的契約而已
🪵 SOLID 的 Liskov Substitution principle 不只是找人,更要找「能勝任的人」
剛才談到,我們應該依賴外部提供的功能性而非在服務內自己寫死所有實作,但若這個依賴並沒有正確的運作呢?
SOLID 裡面其中的 “L” 也就是 Liskov Substitution principle 就是強調這件事情
👩⚖️ Liskov Substitution principle 是什麼?
如果程式碼原本可以使用某個「父類別或介面」,那麼它也應該可以毫無問題地使用所有子類別或實作類別來替代它。
換句話說,抽象可以被它的具體實作所取代,而且程式還要能正常運作! 比如有天老闆喊一聲:「我們專案需要一個 PO!」這個抽象的「PO」角色可以由任何實作來扮演,例如:HumanPO(一般人)、AI_PO(AI 寫的需求)、TrollPO(亂寫需求還放你鴿子)、NoisePO (上班很吵),只要有依據介面實作都行,但
⚠️ 如果注入了一個 TrollPO,導致整個專案爆炸,這就是違反李氏替換原則
雖然依賴的是抽象(PO 契約),但你注入的實作(具體的 PO)不能破壞原本的邏輯與行為期待!
在 DI 的流程中
- 你寫的程式碼只依賴抽象(IPO 介面)
- DI 容器幫你注入實作(例如 HumanPO, AIPo)
- 你預期不管是哪個 PO,系統都能正常跑!
👉 這才符合「李氏替換原則」
當然,公司找人時,注入人手的方法有很多,比如小孩一出生就注定未來會被安排進公司的血緣注入、在路上遇到伯樂,聊天後就進了公司的緣分注入、 被 headhunter 精準推薦進入公司的`獵人頭注入…
🪵 實作一個簡單的 DI 例子
建立服務時,我們會遇到「我需要某個功能,但不在乎是誰提供的」這種情境,這就是依賴注入(Dependency Injection, DI)登場的時刻
🎭 Step 1:定義需求,而不是指定人選
我們先定義一個 RDInterface,任何人只要能實作 WriteCSHARP,我們就承認他是個合格的 RD:
1 |
|
這就像職缺說明:「我們正在找一位會寫 C# 的工程師」,至於來的是誰、是男是女、是內向還是外向,一點都不重要。
👥 Step 2:Team 成立!只招募「能幹活的」
Team 是我們要成立的開發團隊,它只在乎一件事:可以 WriteCSHARP 的 RD DoTheFuckingWork
1 |
|
🧍♂️ Step 3:找人來幹活(注入實作)
現在有一個人選
1 |
|
我們將他注入這個團隊
1 |
|
🔄 Step 4:換個人接手(動態更換依賴)
某天這個 Team 覺得 Bruce 不知道為啥好像很累的樣子,決定換人接手
老闆在路邊抓到一個工程師發現他有 WriteCSHARP 的能力
1 |
|
1 |
|
輸出
1 |
|
雖然新 RD 有點好動,但還是把事情給幹完了,因此皆大歡喜!
從這個例子可以看到,對 Team 來說, Bruce & Winston 具有 Sleep 的特性還是會 RushAroundEverywhere 不重要,他的需僅是具有 WriteCSHARP 方法的人,這樣的設計讓我們可以 彈性更換實作、方便測試、容易擴充。
這也就是 DI 的本質,它讓我們得以分離創建與使用,依賴的是能力(抽象),不是對象本身,當 Team 不再自己處理程式碼,而是接受外部「注入」的人選時,這個 Team 更得更輕盈
若要自動根據情境注入,可以搭配 DI 容器(像 ASP.NET Core 內建的 DI framework)。
DI 在程式設計上的意義還有什麼呢?
🪵 IoC (Inversion of Control)
「依賴注入」是實現「控制反轉」的一種方式,那我們在反轉什麼?
在傳統寫程式的方式裡,主程式像萬能老闆,什麼都自己決定:自己建立物件、自己控制流程、自己處理細節。
而所謂的「控制反轉」就是把這個主導權交給外部系統來幫你安排,你不再主動 new 物件、決定策略,而是交給某個「外部管理者」來幫你處理這些事。這個管理者可能是
- IoC 容器(像是 .NET 的 DI Container)
- 事件派送器(Event Dispatcher)
- 策略註冊中心(Strategy Registry)
👩👦 我媽只說一句:我要去日本。剩下的訂機票、訂飯店、排行程,就靠注入旅行社幫她搞定,當然也可以靠兒子女兒幫他安排,只要達成讓他去日本的目標就好,他不用自己安排所有細節 => Ioc!
以前你想追 YouTuber 的新片,要怎麼辦?自己每天去他的頻道逛,看看有沒有更新,現在只要「訂閱 + 開啟小鈴鐺」,YouTube 系統會自動在適當時機提醒你,被動接收通知 => IoC!
🪵 SOLID 的 Dependency Inversion Principle(依賴反轉原則)
這個原則的核心有兩句經典台詞
✨ 「高層模組不應該依賴低層模組,兩者都應該依賴抽象(Abstraction)。」
✨ 「抽象不應該依賴細節;細節應該依賴抽象。」
假設你有個高層模組叫做 ReportGenerator,它要輸出報表,那它該怎麼處理輸出這件事?
它不應該直接在裡面 new 一個 PdfExporter 或 ExcelExporter,如此一想要改成其他服務就要改動 ReportGenerator 的內容,容易變得手足無措不敢改
建議的做法是
1 |
|
讓 ReportGenerator 只依賴 IExporter 這個「抽象的介面」
1 |
|
- ReportGenerator(高層)只知道有個 IExporter,不管誰來做。
- PdfExporter、ExcelExporter(低層)實作 IExporter,可以互換。
所以「兩邊都依賴抽象(IExporter)」,沒有誰直接依賴誰,彼此不認識
「高層說:我要什麼功能」
「低層說:我來實作這功能」
👉 用抽象當中間橋梁,兩邊都不直接綁死。
🪵 除了 C# 之外
在物件導向(OOP)設計中,例如 C# ,依賴注入的做法非常常見,並且是官方的預設做法,這時候會好奇,其他語言與 DI 的關係是甚麼呢?
函數式語言 (Functional Programming):天然就有「注入」的能力
例如 F#、Haskell 或甚至 JavaScript(部分支援 FP 風格),「函數」本身是一等公民(First-class citizen),也因此不太需要特別使用 OOP-style 的 DI 框架。
「函數是一等公民」意思是,我們可以像操作變數一樣操作函數,也就是把函數當作參數傳進去並當成回傳值傳出來,甚至把函數存進變數、丟進資料結構裡
1 |
|
在這裡,我們把 sayHello 函數「當作參數」傳給另一個函數 runFunction。這就像是把一個依賴塞進去一樣──所以,這裡根本不需要什麼「依賴注入框架」,因為「注入」本身就內建了。
在函數式程式設計中,「依賴注入」不是設計模式,而是語言的基本性質。函數本身就是可被傳遞與組合的依賴單位,與 OOP 有著本質上的不同
動態語言:型別鬆散,依賴可隨時替換
在動態語言(如 Python、JavaScript)中,由於不需要明確宣告變數型別,也可以在執行時任意更換函數或屬性,讓「替換依賴」變得非常簡單
1 |
|
在這裡,我們動態地替換掉原本的函數行為,這在 C#、Java 等靜態語言中就沒那麼容易做到(至少得透過介面、Mock 套件或 DI 框架)。在這些語言中,你可以隨時「偷偷換掉」某個功能的實作,不需要額外的注入設計,自然也不需要嚴格的 DI 容器
這類型的語言比較偏向開發速度第一,讓工程師能夠快速試驗與修改程式,所以通常也沒有強制型別
程序式語言(Procedural Programming):函數寫死地彼此呼叫,沒有注入點
在程序式語言(Procedural Programming)中,例如 C 語言,程式邏輯是由一連串的函數直接呼叫彼此完成。這種寫法沒有「物件」與「抽象」,自然也沒有可以「注入依賴」的空間。
1 |
|
在這裡,processOrder 直接呼叫了 logMessage,兩者已經「寫死」了依賴關係,無法說「我想換個 Logger 來用」,所以也談不上依賴注入。
本質上 C 的哲學不是彈性,而是「精準與控制」
☘️ 結語
在那間森林裡的小木屋裡,老貓早已不再親手備茶生火。牠知道,只要打開門,總會有人帶來需要的東西。這間小屋從不強求擁有全部,卻因為懂得信任與託付,有時程式設計也是如此,我們不必什麼都自己控制,只要相信抽象、設計容器,剩下的,就讓世界來幫我們注入吧

