IoC/DI :
IoC — Inversion of Control,控制反轉
DI — Dependency Injection,依賴注入
IoC,是一種 設計原則:
藉由 『分離組件 (Components) 的設置與使用』,來降低類別或模組之間的耦合度 (i.e., 解耦)。
我的偶像 😍 軟體開發教父 —— Martin Fowler,因認為 “IoC” 的意義易使人困惑,
於 2000 年初,與多位 IoC 提倡者,給予其實作方式一個更具體的名稱
— — "Dependency Injection (依賴注入)"。
IoC/DI 很好的實現 好萊塢原則 (Hollywood Principle)、
依賴倒置原則 (Dependency Inversion Principle, DIP) 、 開閉原則 (Open-Closed Principle) …etc.,
是框架的必備特徵,當然也是各語言主流框架的核心 (e.g. : Spring, Laravel, .Net MVC …) 。
目錄
控制反轉 (Inversion of Control)
有去過 網咖 吧?
網咖,我們曾經的第二個家,有著歡笑、幹瞧、泡麵的桃源鄉。
還有多到哭的遊戲,
譬如:流星蝴蝶劍、天堂、RO、勁舞、世紀帝國、CS、淡水阿給、
三國、信長、守女、國王 TD、跑跑、惡靈勢力、GTA、LOL ⋯⋯ 。(青春再見 😭)
除了當天最新版更新、GGC 出包,或是網咖太爛
想請問: 熱門遊戲 有哪次是你自己下載的?
沒有,幾乎不用。
因為網咖都會提供好。
需要的 遊戲,不用自己 下載,而是 網咖提供 給你。 || 需要的 物件,不用自己 取得,而是 服務容器 提供 給你。 || 需要的 依賴實例,不用 主動 (Active) 建立,而是 被動 (Passive) 接收。
實例依賴物件 的 『控制流程 (Control Flow)』,由 主動 變成 被動。
就是 控制反轉 (Inversion of Control) 。
控制反轉 (Inversion of Control)vs依賴反轉 (Dependency Inversion)
首先:
兩者不相等!
兩者不相等!
兩者不相等!
—— 覺得很重要 o.o
還記得唄?
依賴倒置原則 (Dependency Inversion Principle, DIP) :
- 高階模組不應該依賴於低階模組,兩者都該依賴抽象。
- 抽象不應該依賴於具體實作方式。
- 具體實作方式則應該依賴抽象。
倒轉的是 『依賴關係』。
控制反轉 (Inversion of Control, IoC),
倒轉的則是 實例依賴物件 的『控制流程』。
雖然倆者不相等,卻大有關係啊~~~
唯有倆者合作,才能真正達到 天人合一、超級牛逼 的 鬆散耦合系統。
如果違反 依賴倒置原則 ?
舉例來說,此程式違反了 依賴倒置原則,
它使高階模組 (Computer) 依賴 低階模組 (英雄聯盟):
class Computer {
// 依賴於低階模組:『具體』的英雄聯盟,而非 『抽象』的遊戲
private 英雄聯盟 lol;
public Computer() {
// 預設安裝遊戲: 英雄聯盟
lol = new 英雄聯盟();
}
public Computer(英雄聯盟 lol) {
this.lol = lol;
}
public void playGame() {
if (lol != null)
lol.play();
}
}
class 英雄聯盟 {
public void play() {
System.out.print("德瑪西雅~");
}
}
然而,他還是能透過 IoC/DI ,
被動取得 類別 “英雄聯盟” 的實例 lol:
解除了 高階模組 (Computer) 主動對 低階元件 (英雄聯盟) 的實例方式,
卻 解除不了 高階模組 對 低階模組 的 依賴關係。
因為 高階模組 依賴的是 具體實作 (英雄聯盟),
而非 抽象 (介面 or 抽象類別)。
如果想實現 依賴倒置原則 ?
還記得 依賴倒置原則 (Dependency Inversion Principle, DIP) 的結尾嗎?
僅僅將『 高階模組的依賴對象,由具體改為抽象 』,是 不夠 的,
因為高階模組 欲使用 低階模組的物件時,還是 需要自己 new 具體實作類別。
依賴並未解除
想解除這種依賴,即可透過 IoC/DI ,
直接將 所需低階元件 傳遞給 高階模組使用,
高階模組 啥都沒幹,就可以直接使用 低階模組。
真是
潮爽 der ~~
兩者結合後也就是:
高階模組,依賴於抽象,而非低階模組。
但 要使用 該抽象的 具體產品 (低階模組) 時,
- 不用也不需要知道是哪種 具體產品
- 不再自己實例 具體產品,而是 服務容器 會提供給他 。
好萊塢原則 (Hollywood Principle)
謎:一堆高階、低階、具體的,到底在公尛叮噹 😵。
好啦,不裝逼,講些實際的例子。
- 小碰友 不需要 自己賺錢,而是 爸媽 會給他。
- 我 不需要想 要吃什麼 早餐 和 自己去買,而是 女朋友 準備好給我。 (別告訴我,其實我沒有女朋友😭)
- 你 去廁所大便,不用想 要買哪牌 的衛生紙, 也不用自己準備 , 而是 廁所 提供給你。
- 我們 去網咖 ,不用想 要有哪些 遊戲,也不用自己下載,而是 網咖 會提供給你。
- … 。
怎樣,聽起來有沒有好方便,而且潮爽 der~ ?
IoC/DI ,揪 4 這麼 爽 U ~
由 服務容器 (IoC 容器) 透過 “依賴注入”,給予 高階模組 所需得具體產品:
取代傳統的主動建立實例:
[註]:為了避免造成誤會,
這邊指的 “建立” 是指 create,用 uml 來看也就是:
高階模組 不再去 "找尋" 插件,
而是插件會自己注入到 高階模組中。
這就是 好萊塢原則 (Hollywood Principle):
不要找我們,我們會找你。 (Don’t call us, we’ll call you.)
但是得知道:
IoC/DI ,並非實現 "DIP" 的唯一解,
還有 工廠方法模式 (Factory Method Pattern)、服務定位模式 (Service Locator Pattern),
以及各種的 建立型模式 (Creational Pattern)…,皆可以消除 實例具體產品 (插件 Plugin) 的依賴關係。
依賴注入 (Dependency Injection)
顧名思義:
將所需的 依賴實例,注入到高階模組中。
其實每個人都用過啦,只是名字很裝逼。
有以下三種形式:
- 建構元注入 (Constructor Injection)
- 設值方法注入 (Setter Injection)
- 介面注入 (Interface Injection)
這裡我以 “網咖” 做為舉例,
高階模組 (Computer) 依賴 抽象介面 (Game),
且有許多 具體產品 (Plugin) 實作此 抽象介面 (Game):
class Computer implements GameInjector {
private Game game; // 依賴 『抽象』,而非『具體』
// 建構元注入 (Constructor Injection)
public Computer(Game game) {
this.game = game;
}
// 設值方法注入 (Setter Injection)
public void setGame(Game game) {
this.game = game;
}
// 介面注入 (Interface Injection)
@Override
public void injectGame(Game game) {
this.game = game;
}
public void playGame() {
if (game != null) {
game.play();
}
}
}
// 遊戲注入者
// 可以規範: 任何需要 "遊戲" 的模組 都必須實做此介面
interface GameInjector{
void injectGame(Game game);
}
UML 類別圖:
可以看到程式中,沒有任何 “具體實作類別” 的名稱,
而是由 依賴注入 取得 插件實例,
高階模組,完全沒有與具體實作 耦合,
實現了 依賴倒置原則 (Dependency Inversion Principle, DIP) !
建構元注入 (Constructor Injection) vs 設值方法注入 (Setter Injection)
這兩種都很常見,那實際上用哪種方式最好呢?
Martin 說話了:
It’s important to support both mechanisms, even if there’s a preference for one of them.
—— Martin Fowler
即使你有較偏好的選擇,同時支持這兩種機制都是必要的。
IoC/DI vs 工廠方法模式 (Factory Method Pattern)
許多人會感到困惑:
「控制反轉 與 工廠,不都可以讓 高階模組 取得 所需插件嗎?
那倆者到底怎麼抉擇呢?」
如果你有以上的疑問,
代表: 對 工廠方法模式 的觀念還不夠熟悉喔~
工廠只負責『生產』,不牽涉到 實例依賴物件的 『控制流程』。
高階模組是否依賴於工廠,全憑你怎麼使用它、在哪使用它。
舉幾個簡單的例子:
1. 傳統控制流程的 『使用工廠 實例插件』:
class Computer {
private Game game;
public Computer() {
GameFactory factory = new ImplGameFactory(); // 實例 遊戲工廠 (注意多型)
this.game = factory.createGame(); // 透過工廠,取得遊戲
}
...
}
2. 控制反轉 的 『使用工廠 實例插件』 — [型一] 插件注入:
public class Main {
public static void main(String[] args) {
GameFactory factory = new ImplGameFactory(); // 實例 遊戲工廠 (注意多型)
Game game = factory.createGame(); // 透過工廠,取得遊戲
Computer computer = new Computer(game); // game 依賴注入
}
}
class Computer {
private Game game;
// 建構元注入 (Constructor Injection)
public Computer(Game game) {
this.game = game;
}
...
}
3. 控制反轉 的 『使用工廠 實例插件』 — [型二] 工廠注入:
public class Main {
public static void main(String[] args) {
GameFactory factory = new ImplGameFactory(); // 實例 遊戲工廠 (注意多型)
Computer computer = new Computer(factory); // factory 依賴注入
}
}
class Computer {
private Game game;
// 建構元注入 (Constructor Injection)
public Computer(GameFactory factory) {
this.game = factory.createGame(); // 透過工廠,取得遊戲
}
...
}
可以看到
第一種、傳統控制流程的 『使用工廠 實例插件』:
高階模組 (Computer) 不依賴於 低階模組,而是依賴於抽象 (Game)。 (Good)
高階模組 (Computer) 依賴於 抽象 (GameFactory)。 (Good)
高階模組 (Computer) 依賴於 具體實作工廠 (ImplGameFactory)。 (Bad)
第二種 及 第三種 的方式,
一樣使用了工廠,卻可解除 高階模組 與 具體工廠 的依賴關係!
有些人說:
「工廠方法模式 (Factory Method Pattern) ,解除了 高階模組 與 低階模組的依賴,
但其缺點是,高階模組 必須實例其 具體實作工廠 的子類,
當擴充新產品時,還是必須修改 高階模組,因此違反了 開閉原則 (Open-Closed Principle)。」
恩...我們祝福他
IoC 容器 (IoC Container)
IoC 容器 (又稱: 服務容器 Service Container),
是 組裝 & 配置元件,透過 依賴注入 (Dependency Injection),
提供所需服務給模組的地方。
廣義上來說, IoC 容器,就是有進行 依賴注入 的地方,
你隨便寫一個類別,透過它將所需元件注入給 高階模組,便可說是容器。
但現在所說的 『容器』,往往是泛指那些強大『框架』的容器:
根據設定『自動生產』物件 (非單一產品),將其提供給所需模組,
並管理該物件整個生命週期的 超級自動化工廠。
大部分框架的 IoC 容器,幾乎都透過 『反射 (Reflection) 機制』,
來 動態生成實例、或由配置文件 (e.g., json、xml、properties、ini、php) 尋找依賴關係,
描述該如何建構實例、或檢查是否該為當前模組注入依賴 …。
因此容器,通常會有個 bind (或 register、config …etc.) 的函數,
供 註冊依賴關係、或告訴容器: 何種情境需要該實例。
再來,通常也會有有個 make (或 create、resolve …etc.),
讓容器 解析物件,或實例已綁定之物件。
網咖容器範例
接續以 "網咖" 作為例子,
我 粗略地 模擬了一個容器,
但是超級低級,實務上完全派不上用場,連物件生命週期都沒管理 😂,
寫得很簡單,just 讓你感受一下味道而已。
實際的容器,需要兼顧很多細節,
並且根據不同 語言、功能、用途,容器的寫法也不盡相同,
大部分框架都寫得很棒,我就不重造輪子了。
有興趣的再看即可,高手可以跳過 (非常簡單),
網咖容器範例 原始碼:
網咖容器範例 點我觀看
實際上的使用:
public class Main {
public static void main(String[] args) {
ServiceContainer container = new ServiceContainer(); // 實例 容器
// 註冊依賴關係: 當遇到 『 Game 』,讓容器給我 『 爆爆王 』
container.bind(Game.class, 爆爆王.class);
// 註冊依賴關係: 當遇到 『 Computer 』,讓容器給我 『 Computer(Game, 100) 』
// 並且 因為上面已註冊過 Game , 容器會直接傳回『 爆爆王 』給我
container.bind(Computer.class, Game.class, 100);
Computer computer = (Computer) container.make(Computer.class); // 讓容器製作 Computer
computer.playGame(); // 使用已被注入依賴的 Computer 物件
}
}
// Result:
/*
* 開始實例物件: Computer
* 建構元數量為4
*
* 尋訪 0 參數 建構元
* 未綁定0 參數
*
* 尋訪 1 參數 建構元
* 未綁定1 參數
*
* 尋訪 2 參數 建構元
* 尋訪 被綁定參數 list
* 參數型態: Game
* 進入 make 遞迴 實例參數
* 開始實例物件: Game
* 建構元數量為0
*
* 爆爆王 為 Game 的子類!
* ====實例物件====
* 爆爆王
* ===============
*
* 實例 Game 成功
*
* boolean
* Integer
* ===============
* 建構參數 與 綁定 list 數量相同,但型態不同 (多載)
* 略過此次迴圈尋訪
* ===============
*
* 尋訪 2 參數 建構元
* 尋訪 被綁定參數 list
*
* 參數型態: Game
* 進入 make 遞迴 實例參數
* 開始實例物件: Game
* 建構元數量為0
*
* 爆爆王 為 Game 的子類!
* ====實例物件====
* 爆爆王
* ===============
*
* 實例 Game 成功
*
* 此遊戲需要 100元
* 實例 Computer 成功
*
* 海盜船 14
*/
可以看到上面,範例中,
第一個參數,為要綁定的『條件』,
XXX.class ,是我做的簡易『型別提示』,
其他皆是 建構子的參數。
以 bind(Computer.class, Game.class,100)
為例,
第一個參數,為要綁定的『條件』,
後面都是建構元 參數。
但是,實際上的建構子,是長這樣:
public Computer(Game game, Integer money){...
需傳入的,是 Game 實例,而非 Game.class,
容器會去解析 Game.class,看有沒有相關的 具體實作,幫你依賴注入。
再舉個例:
public class Main {
public static void main(String[] args) {
ServiceContainer container = new ServiceContainer(); // 實例 容器
// 註冊依賴關係: 當遇到 『 Game 』,讓容器給我 『 英雄聯盟 』
container.bind(Game.class, 英雄聯盟.class);
// 註冊依賴關係: 當遇到 『 Computer 』,讓容器給我 『 Computer(Game,true) 』
// 並且 因為上面已註冊過 Game , 容器會直接傳回『英雄聯盟』給我
container.bind(Computer.class, Game.class, true);
Computer computer1 = (Computer) container.make(Computer.class); // 讓容器製作 Computer
computer1.playGame(); // 使用已被注入依賴的 Computer 物件
}
}
// Result:
/*
* 開始實例物件: Computer
* 建構元數量為4
*
* 尋訪 0 參數 建構元
* 未綁定0 參數
*
* 尋訪 1 參數 建構元
* 未綁定1 參數
*
* 尋訪 2 參數 建構元
* 尋訪 被綁定參數 list
*
* 參數型態: Game
* 進入 make 遞迴 實例參數
* 開始實例物件: Game
* 建構元數量為0
*
* 英雄聯盟 為 Game 的子類!
* ====實例物件====
* 英雄聯盟
* ===============
*
* 實例 Game 成功
*
* 這是線上遊戲
* 實例 Computer 成功
*
* 德瑪西雅~
*/
總結
誒~ 不對啊
我只是要開發個 小系統,又不想學這個框架,
難道要自刻一個容器,才能使用 IoC/DI 嗎?
太鏘了吧 = =…
非也
上面有提到,不同語言、不同用途,容器寫法不盡相同,
所以我極度建議:
不要沒事造輪子
沒用框架也沒差,
要記得 『控制反轉 』指的是:
反轉『實例物件的控制流程』,
並非一定要有框架般的強大容器,才做得到,這是個錯誤的迷思~ 😮。
簡單加入一個 『第三方類別』進行實例元件、依賴注入,就可以達到。
這時 工廠方法模式 (Factory Method Pattern),就非常好用。
(再次強調,工廠 與 IoC/DI 並非互斥,甚至時常透過 工廠 實現 IoC/DI)
如果熟悉了 IoC/DI 的概念,
就知道了,學習框架,除了學習他的運作流程、提供的方法…,
再來就是學習,如何讓它 自動 調用你寫的類別,
這也是 框架 (Framework) 與 函式庫 (Library) 最大的差異之處。
範例原始檔
在《控制反轉 (IoC) 與 依賴注入 (DI)》中有 29 則留言
寫得真好
感恩 😬
不是寫程式的我也看不懂😂但以設計系的角度看,版面乾淨:))很清楚明瞭
哈哈 謝謝囉 😆
清晰易懂,例子也很有趣~學起來了
謝謝您 😁
我的榮幸
覺得棒棒der~
幽默又有深度的講解!!!已跪
Reflaction 應該改為 Reflection ?
感謝! 已修正
請問有推薦的好書嗎 或資料來源嗎? 或者能分享你自己理解這些東西的方法嗎? 謝謝
最佳地理解方式就是考察來源囉!
此外,就是常年使用各框架的些許心得吧 😄
IoC/DI 來自 Martin Fowler 此篇著名文章:
https://martinfowler.com/articles/injection.html
請問一下第二篇工廠模式的文章為什麼會刪掉? 謝謝
內容升級中 😂
補充更多實作細節~
(原文似乎過於簡易)
非常抱歉,造成您的不便😅
真的很棒 XD
以後可以出書了
以前似懂非懂的觀念被釐清了不少
真的很感謝您的鼓勵!😭
但還沒這麼厲害,可能要再好幾年 😂
寫太好 敲碗等更
感謝支持🙏🙏🙏
最近剛開始接觸這塊
一直搞不太懂
你的說明替我解惑了不少
太感謝了
很開心能幫到您 😇
觀念釐清了不少呢
請教一個問題,當高階模組達成DIP時是否也隱含了IoC/DI的概念呢? 因為當高階模組依賴抽象時代表它也new不出任何實例出來了,這時必定是等外部注入才能正常運作。請問我這樣的認知有錯誤嗎?
2020 年發現這篇文章,寫得很不錯
很清楚耶
排版也很乾淨
學到了 非常感謝!
神作
有些人說:
「工廠方法模式 (Factory Method Pattern) ,解除了 高階模組 與 低階模組的依賴,
但其缺點是,高階模組 必須實例其 具體實作工廠 的子類,
當擴充新產品時,還是必須修改 高階模組,因此違反了 開閉原則 (Open-Closed Principle)。」
恩…我們祝福他
想請教一下,這樣是有代表甚麼涵義嗎?
是指這個缺點不對嗎
看了都想回去打網咖了