Image

森林的深處,有一間小木屋。每當霧氣升起,樹葉發出細碎的聲響,動物們會緩緩走進這裡 —— 這裡不熱鬧,卻總是剛剛好。

一隻老貓躺在椅上,望著壁爐裡跳動的火光,輕輕說:「我們啊,無法事事都靠自己。」
這裡的每一份溫暖,都不是自己製造的,而是來自彼此的信任與協作。
柴火是兔子帶來的,窗簾是松鼠縫的,茶是隔壁小鹿泡的。屋子本身沒有準備這一切 —— 但正因為如此,它才如此輕盈。

我們不在類別中自行建構一切,也不強求自己了解每個細節。我們只需要設計出一個能被好好照顧的容器,然後放手交給外界來注入所需的一切。



🪵 怎麼理解 DI

你都怎麼叫同事?

👨‍💻 RD:欸 PO
📊 PO:怎樣 RD?

看起來好像有點冷淡,對吧?彼此之間只用職稱互稱,完全不知道對方是誰、是高是矮、是圓的扁的。但這其實就蘊含了 Dependency Injection 的精髓:

他們之間的互動,是依賴「抽象(角色)」,而不是「具體(人名)」

PO 並不在乎對面的 RD 是哪個腦殘,他只關心有能當 RD 的人,誰來都行,只要符合這個職責就好。這就是所謂的依賴抽象(依賴介面),而不是綁定具體對象。



🪵 李氏替換不只是找人,更要找「能勝任的人」


👩‍⚖️ 李氏替換原則是什麼?

如果程式碼原本可以使用某個「父類別或介面」,那麼它也應該可以毫無問題地使用所有子類別或實作類別來替代它。

簡單說就是,抽象可以被它的具體實作所取代,而且程式還要能正常運作。

有天老闆喊一聲:「我要一個 PO!」這個抽象的「PO」角色可以由任何實作來扮演,例如:HumanPO(一般人)、AI_PO(AI 寫的需求)、TrollPO(亂寫需求還放你鴿子)

⚠️ 如果你注入了一個 TrollPO,導致整個專案爆炸,這就是違反李氏替換原則。

也就是說,你雖然依賴的是抽象(PO 介面),但你注入的實作(具體的 PO)不能破壞原本的邏輯與行為期待!

在 DI 的流程中:

  • 你寫的程式碼只依賴抽象(IPO 介面)
  • DI 容器幫你注入實作(例如 HumanPO, AIPo)
  • 你預期不管是哪個 PO,系統都能正常跑!

👉 這樣才符合「李氏替換原則」。

Image

當然注入手法有很多,家族企業,小孩一出生就被安排進公司的血緣注入、在路上遇到伯樂,聊天後就進了公司的緣分注入、 被 headhunter 精準推薦進入公司的獵人頭注入…



🪵 實作一個簡單的 DI!


在寫程式的過程中,常常會遇到「我需要某個功能,但不在乎是誰提供的」這種情境。這就是依賴注入(Dependency Injection, DI)登場的時刻。


🎭 Step 1:定義需求,而不是指定人選

我們先定義一個 RDInterface,任何人只要能實作 WriteCSHARP,我們就承認他是個合格的 RD:

1
2
3
4
5
6

public interface RDInterface
{
void WriteCSHARP();
}

這就像職缺說明:「我們正在找一位會寫 C# 的工程師」,至於來的是誰、是男是女、是內向還是外向,一點都不重要。



👥 Step 2:Team 成立!只招募「能幹活的」

Team 是我們要成立的開發團隊,它只在乎一件事:可以 WriteCSHARP 的 RD DoTheFuckingWork

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public class Team
{
private RDInterface _RD;

public Team(RDInterface rD)
{
_RD = rD;
}

public void SetRD(RDInterface newRD)
{
System.Console.WriteLine("換換口味...");
_RD = newRD;
}

public void DoTheFuckingWork()
{
_RD.WriteCSHARP();
}
}



🧍‍♂️ Step 3:找人來幹活(注入實作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public class Bruce : RDInterface
{
private readonly PersonalityTrait trait = PersonalityTrait.DetailOriented;
public void WriteCSHARP()
{
System.Console.WriteLine("Write and feel sleepy...");
}

public void Sleep(int hours)
{
System.Console.WriteLine($"Bruce is sleeping for {hours} hours...");
}
}

我們將 Bruce 注入 Team:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

//// 工作流程
var brucezzz = new Bruce();
var team = new Team(brucezzz);
team.DoTheFuckingWork();

var random2 = new Random();
for (int i = 0; i < 5; i++)
{
brucezzz.Sleep(random2.Next(1, 10));
}

team.DoTheFuckingWork();
for (int i = 0; i < 5; i++)
{
brucezzz.Sleep(random2.Next(1, 10));
}


//// Write and feel sleepy...
//// Bruce is sleeping for 6 hours...
//// (…)
//// Write and feel sleepy...



🔄 Step 4:換個人接手(動態更換依賴)

某天這個 Team 覺得 Bruce 不知道為啥很累的樣子,就換人接手幫忙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public class Winston : RDInterface
{
private readonly PersonalityTrait trait = PersonalityTrait.Impulsive;
public void WriteCSHARP()
{
System.Console.WriteLine("Running while Writing...");
}

public void RushAroundEverywhere()
{
System.Console.WriteLine($"在公司跑來跑去....");
}
}

1
2
3
4
5
6
7
8
9

var win = new Winston();
team.SetRD(win);
team.DoTheFuckingWork();
win.RushAroundEverywhere();
team.DoTheFuckingWork();
win.RushAroundEverywhere();
team.DoTheFuckingWork();



輸出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Write and feel sleepy...

Bruce is sleeping for 6 hours...
Bruce is sleeping for 1 hours...
Bruce is sleeping for 2 hours...
Bruce is sleeping for 7 hours...
Bruce is sleeping for 2 hours...

Write and feel sleepy...

Bruce is sleeping for 7 hours...
Bruce is sleeping for 7 hours...
Bruce is sleeping for 1 hours...
Bruce is sleeping for 9 hours...
Bruce is sleeping for 2 hours...

換換口味...

Running while Writing...
在公司跑來跑去忘了帶電腦....
Running while Writing...
在公司跑來跑去忘了帶電腦....
Running while Writing...

隨然新 RD 有點好動,但總算還是把事情給幹完了皆大歡喜

可以看到,對一個 Team 來說,他具有 Sleep 的特性還是會 RushAroundEverywhere 不重要,他的需其僅是具有 WriteCSHARP 方法的人

我們可以觀察到:

  • Team 只依賴 抽象(RDInterface),不在乎是 Bruce 還是 Winston。
  • 只要實作了 WriteCSHARP(),誰都可以成為這個 Team 的成員。
  • 這樣的設計讓我們可以 彈性更換實作、方便測試、容易擴充。

這就是 DI 的本質:

  • 分離創建與使用,依賴的是能力(抽象),不是對象本身。
  • 當 Team 不再自己決定誰來做事,而是接受外部「注入」的人選時,它變得更輕盈、更具彈性,就像那間森林裡的小木屋,不需要自己準備所有資源,卻因為協作而變得溫暖又豐富。


若你有多個職缺,可以把這些介面抽象設計再進一步細分(例如加上 IFrontend, IDevOps)。
若你要自動根據情境注入,可以搭配 DI 容器(像 ASP.NET Core 內建的 DI framework)。



🪵 IoC


「依賴注入」是實現「控制反轉」的一種方式。

在傳統寫程式的方式裡,主程式像個萬能老闆,什麼都自己決定:自己建立物件、自己控制流程、自己處理細節。
而所謂的「控制反轉」就是:把這個主導權交給外部系統來幫你安排。

你不再主動 new 物件、決定策略,而是交給某個「外部管理者」來幫你處理這些事。這個管理者可能是:

  • IoC 容器(像是 .NET 的 DI Container)
  • 事件派送器(Event Dispatcher)
  • 策略註冊中心(Strategy Registry)

👩‍👦「我媽只說一句:我要去日本。剩下的訂機票、訂飯店、排行程,全都是旅行社幫她搞定。」 => IoC!

以前你想追 YouTuber 的新片,要怎麼辦?自己每天去他的頻道逛,看看有沒有更新。現在只要「訂閱 + 開啟小鈴鐺」YouTube 系統會自動在適當時機提醒你。你被動接收通知 => IoC!



🪵 Dependency Inversion Principle(依賴反轉原則)

這個原則的核心有兩句經典台詞:

✨ 「高層模組不應該依賴低層模組,兩者都應該依賴抽象(Abstraction)。」
✨ 「抽象不應該依賴細節;細節應該依賴抽象。」

這句話聽起來很像哲學,其實它的意思很簡單:你寫程式時,不要直接依賴「某個具體的人來幫你做事」,
而是依賴你想要的功能長什麼樣子(介面),誰來做都可以換!

假設你有個高層模組叫做 ReportGenerator,它要輸出報表,那它該怎麼處理輸出這件事?

它不應該直接綁定 PdfExporter 或 ExcelExporter,因為這樣一改格式就要改高層邏輯,超麻煩!

正確做法是:

1
2
3
4
5
6

public interface IExporter
{
void Export(string content);
}

讓 ReportGenerator 只依賴 IExporter 這個「抽象的出口」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public class ReportGenerator
{
private readonly IExporter _exporter;

public ReportGenerator(IExporter exporter)
{
_exporter = exporter;
}

public void GenerateReport()
{
var report = "這是報表內容";
_exporter.Export(report);
}
}

  • ReportGenerator(高層)只知道有個 IExporter,不管誰來做。
  • PdfExporter、ExcelExporter(低層)實作 IExporter,可以互換。

所以「兩邊都依賴抽象(IExporter)」,沒有誰綁誰。

「高層說:我要什麼功能」
「低層說:我來實作這功能」

👉 用抽象當中間橋梁,兩邊都不直接綁死。



🪵 「依賴注入(DI)」是物件導向設計的重要概念?

在物件導向(OOP)設計中,「依賴注入(Dependency Injection)」是一個常見的設計模式,用來解決模組之間的耦合問題,並提升系統的彈性與可測試性。

但你可能會問:「為什麼要特別說是 OOP 呢?在其他程式語言或設計方式中,DI 不也可以使用嗎?」

接下來,我們從幾種不同的程式設計範式(尤其是函數式與程序式)來探討這個問題。


函數式語言:天然就有「注入」的能力

函數式程式語言(Functional Programming)中,例如 F#、Haskell 或甚至 JavaScript(部分支援 FP 風格),「函數」本身是一等公民(First-class citizen),也因此不太需要特別使用 OOP-style 的 DI 框架。


什麼是「函數是一等公民」?


  • 這表示你可以像操作變數一樣操作函數:
  • 把函數當作參數傳進去
  • 把函數當成回傳值傳出來
  • 把函數存進變數、丟進資料結構裡
1
2
3
4
5
6
7
8
9
10
11

function sayHello() {
console.log("Hello!");
}

function runFunction(fn) {
fn(); // 呼叫傳進來的函數
}

runFunction(sayHello); // 輸出:Hello!

這段程式中,我們把 sayHello 函數「當作參數」傳給另一個函數 runFunction。這就像是把一個依賴塞進去一樣──所以,這裡根本不需要什麼「依賴注入框架」,因為「注入」本身就內建了。

函數式語言(Functional Programming, FP)是一種強調「純函數(pure functions)」與「資料不可變(immutability)」的程式設計方式。



動態語言:型別鬆散,依賴可隨時替換

在動態語言(如 Python、JavaScript)中,由於不需要明確宣告變數型別,也可以在執行時任意更換函數或屬性,讓「替換依賴」變得非常簡單。

1
2
3
4
5
6
7
8
9
10
11
12

class EmailService:
def send(self, msg):
print("Sending email:", msg)

email = EmailService()
email.send("Hello") # 輸出:Sending email: Hello

# 替換成假的函數(mock)
email.send = lambda msg: print("FAKE email:", msg)
email.send("Hello") # 輸出:FAKE email: Hello

在這裡,我們動態地替換掉原本的函數行為,這在 C#、Java 等靜態語言中就沒那麼容易做到(至少得透過介面、Mock 套件或 DI 框架)。在這些語言中,你可以隨時「偷偷換掉」某個功能的實作,不需要額外的注入設計,自然也不需要嚴格的 DI 容器。

這類型的語言比較偏向開發速度第一,讓工程師能夠快速試驗與修改程式,所以通常也沒有強制型別



程序式語言:函數寫死地彼此呼叫,沒有注入點

在程序式語言(Procedural Programming)中,例如 C 語言,程式邏輯是由一連串的函數直接呼叫彼此完成。這種寫法沒有「物件」與「抽象」,自然也沒有可以「注入依賴」的空間。

1
2
3
4
5
6
7
8
9

void logMessage(const char* message) {
printf("Log: %s\n", message);
}

void processOrder() {
logMessage("Order processed.");
}

在這裡,processOrder 直接呼叫了 logMessage,兩者已經「寫死」了依賴關係,無法說「我想換個 Logger 來用」,所以也談不上依賴注入。

本質上 C 的哲學不是彈性,而是「精準與控制」



DI 發揮不了作用的情境總結:

  • 沒有抽象(沒有 interface)
  • 沒有注入點(函數或物件直接建立其他物件)
  • 沒有彈性替換依賴的設計(寫死了)


☘️ 結語

在那間森林裡的小木屋裡,老貓早已不再親手備茶生火。牠知道,只要打開門,總會有人帶來需要的東西。這間小屋從不強求擁有全部,卻因為懂得信任與託付,而擁有了最完整的安穩。

寫程式也是如此。我們不必什麼都自己控制,只要相信抽象、設計容器,剩下的,就讓世界來幫我們注入吧。