設計模式/原則

控制反轉 (IoC) 與 依賴注入 (DI)

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)

  1. 高階模組不應該依賴於低階模組,兩者都該依賴抽象
  2. 抽象不應該依賴於具體實作方式。
  3. 具體實作方式則應該依賴抽象。

倒轉的是 『依賴關係』。
 
控制反轉 (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 ~~

 
兩者結合後也就是:

高階模組,依賴於抽象,而非低階模組。
但 要使用 該抽象的 具體產品 (低階模組) 時,

  1. 不用也不需要知道是哪種 具體產品
  2. 不再自己實例 具體產品,而是 服務容器 會提供給他 。

 
 

好萊塢原則 (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)

顧名思義:

將所需的 依賴實例,注入到高階模組中。

 
其實每個人都用過啦,只是名字很裝逼。
有以下三種形式:

  1. 建構元注入 (Constructor Injection)
  2. 設值方法注入 (Setter Injection)
  3. 介面注入 (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 類別圖:
ioc-di-dependency2
 
可以看到程式中,沒有任何 “具體實作類別” 的名稱,
而是由 依賴注入 取得 插件實例,
高階模組,完全沒有與具體實作 耦合,
實現了 依賴倒置原則 (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 (或 registerconfig …etc.) 的函數,
註冊依賴關係、或告訴容器: 何種情境需要該實例。
 
再來,通常也會有有個 make (或 createresolve …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)》中有 22 則留言

  1. 請問有推薦的好書嗎 或資料來源嗎? 或者能分享你自己理解這些東西的方法嗎? 謝謝

    1. 內容升級中 😂
      補充更多實作細節~
      (原文似乎過於簡易)

      非常抱歉,造成您的不便😅

發表迴響