自救必看三大準則

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

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
還記得唄?
  1. 高階模組不應該依賴於低階模組,兩者都該依賴抽象
  2. 抽象不應該依賴於具體實作方式。
  3. 具體實作方式則應該依賴抽象。
倒轉的是 『依賴關係』。

控制反轉 (Inversion of Control, IoC)
倒轉的則是 實例依賴物件 的『控制流程』。

雖然倆者不相等,卻大有關係啊~~~
唯有倆者合作,才能真正達到 天人合一、超級牛逼 的 鬆散耦合系統。

如果違反 依賴倒置原則 ?

舉例來說:
此程式,違反了 依賴倒置原則,高階模組 (Computer) 依賴 低階模組 (英雄聯盟):
class Computer {

    
    private 英雄聯盟 lol;  // 依賴於低階模組:『具體』的英雄聯盟,而非 『抽象』的遊戲

    public Computer() {

        // 預設安裝遊戲: 英雄聯盟
        lol = new 英雄聯盟();
    }

   public void playGame() {
        if (lol != null) {
            lol.play();
        }
    }

}

class 英雄聯盟 {

    @Override
    public void play() {
        System.out.print("德瑪西雅~");
    }
}

// Result:
// 德瑪西雅~

但,他還是能透過 IoC/DI ,
被動取得 類別 “英雄聯盟” 的實例 lol:
解除了 高階模組 (Computer) 主動對 低階元件 (英雄聯盟) 的實例方式,
卻 解除不了 高階模組 對 低階模組 的 依賴關係
因為 高階模組 依賴的是 具體實作 (英雄聯盟),
而非 抽象 (介面 or 抽象類別)。

如果想實現 依賴倒置原則 ?

僅僅將『 高階模組的依賴對象,由具體改為抽象 』,是 不夠 的,
因為高階模組 欲使用 低階模組的物件時,還是 需要自己 new 具體實作類別
依賴並未解除

想解除這種依賴,即可透過 IoC/DI ,
直接將 所需低階元件 傳遞給 高階模組使用,
高階模組 啥都沒幹,就可以直接使用 低階模組。

真是
潮爽 der ~~
兩者結合後也就是:
高階模組,依賴於抽象,而非低階模組。
但 要使用 該抽象的 具體產品 (低階模組) 時,
  1. 不用也不需要知道是哪種 具體產品
  2. 不再自己實例 具體產品,而是 服務容器 會提供給他 。

好萊塢原則 (Hollywood Principle)

謎:一堆高階、低階、具體的,到底在公尛叮噹 😵

好啦,不裝逼,講些實際的例子。
  • 小碰友 不需要 自己賺錢,而是 爸媽 會給他。
  •  不需要想 要吃什麼 早餐 和 自己去買,而是 女朋友 準備好給我。 (別告訴我,其實我沒有女朋友😭)
  •  去廁所大便,不用想 要買哪牌 的衛生紙, 也不用自己準備 , 而是 廁所 提供給你。
  • 我們 去網咖 ,不用想 要有哪些 遊戲,也不用自己下載,而是 網咖 會提供給你。
  • … 。

怎樣,聽起來有沒有好方便,而且潮爽 der~ ?

IoC/DI ,揪 4 這麼 爽 U ~
由 服務容器 (IoC 容器) 透過 “依賴注入”,給予 高階模組 所需得具體產品:

取代傳統的主動建立實例:


p.s 為了避免造成誤會,
這邊指的 “建立” 是指 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

可以看到程式中,沒有任何 “具體實作類別” 的名稱,
而是由 依賴注入 取得 插件實例,
高階模組,完全沒有與具體實作 耦合,

建構元注入 (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) 最大的差異之處。


範例原始檔

出處:https://blog.jason.party/3/ioc-di

留言

這個網誌中的熱門文章

IIS - ASP.NET 網站基本優化設定

Node.js 部署至 IIS 站台

遇見 Parameters 參數上限之大量資料寫入方法