IoC 容器

MasterPoser發表於2020-04-30

梳理 IOC 容器 與 依賴注入

依賴注入:

依賴注入的理解,舉個例子,假設我是一個鋼鐵俠,我需要滿足打壞人的需求,對於我來說我依賴於戰衣(畢竟我沒有戰衣和普通人一樣).

所謂“依賴”,就是 “我若依賴你,我就不能離開你”。

以簡單的例子來說這個依賴不算什麼,如果這個量級很恐怖,我需要滿足打全世界的壞人,各種能力的壞人出現,在很大的依賴體系下,依賴是多麼恐怖.當然既然我提到了這個問題,肯定有解決方案.

##可怕的依賴
接上個例子, 我是鋼鐵俠,我需要按照不同的敵人制作不同的戰衣:

//戰衣
class Armor
{
    //飛行
    protected  $flight;
    //GPS
    protected $gps;
    //等離子發射器
    protected $plasma;

    public function __construct($flight, $gps, $plasma){}

}

假設我製造的戰衣的能力:

  • 斥力發射器

  • GPS全球定位系統

  • 等離子發射器

    //飛行
    class Flight
    {
    //飛行高度
    protected $height;
    
    //飛行速度
    protected $speed;
    
    public function __construct($height, $speed){}
    }
    //GPS
    class Gps
    {
    //精度
    protected $accuracy;
    
    public function __construct($accuracy){}
    }
    //等離子發射器
    class Plasma
    {
    //範圍
    protected $range;
    public function __construct($range){}
    }

    現在我需要在初始化的時候根據敵人能力得到相應的戰衣.

    //鋼鐵俠
    class IronMan
    {
    //斥力發射器GPS全球定位系統等離子發射器
    //能力
    protected $power;
    function __construct()
    {
        $this->power = new Flight('12', '20');
    }
    }

    可以看出來當能力很多的時候,我要一一例項,將會是一個噩夢,現在我的戰衣的屬性是斥力發射器、GPS全球定位系統、等離子發射器。如果需求變更,需要安全性、防禦能力、還要破壞力等一系列需求。我還要一一例項,等我例項出來了,敵人會等我嗎? 我拒絕!!!
      在如此效率低下的情況下,我們不能固化戰衣初始化的能力,而轉由外部負責,由外部創造戰衣屬性的模組、飛行或者等離子發射器等(我們後面統一稱為 “模組”),植入車的某一個介面,這個介面是一個既定的,只要這個 “模組” 滿足這個介面的裝置都可以被鋼鐵俠所利用,可以提升、增加鋼鐵俠的某一種屬性。這種由外部負責其依賴需求的行為,我們可以稱其為 “控制反轉(IoC)”。

    控制反轉(Inversion of Control,縮寫為IoC),是物件導向程式設計中的一種設計原則,可以用來減低計算機程式碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI),還有一種方式叫“依賴查詢”(Dependency Lookup)。通過控制反轉,物件在被建立的時候,由一個調控系統內所有物件的外界實體將其所依賴的物件的引用傳遞給它。也可以說,依賴被注入到物件中。

工廠模式,依賴轉移

 當然,實現控制反轉的方法有幾種。在這之前,不如我們先了解一些好玩的東西。

我們可以想到,元件、工具,是一種可被生產的玩意兒,生產的地方當然是 “工廠(Factory)”,於是有人就提出了這樣一種模式: 工廠模式。
工廠模式,顧名思義,就是一個類所依賴的外部事物的例項,都可以被一個或多個 “工廠” 建立的這樣一種開發模式,就是 “工廠模式”。

我們為了給超人制造超能力模組,我們建立了一個工廠,它可以製造各種各樣的模組,且僅需要通過一個方法:

//戰衣工廠
class ArmorModuleFactory
{
    public function makeModule($moduleName, $options)
    {
        switch ($moduleName) {
            case 'Flight':   return new Flight($options[0], $options[1]);
            case 'Gps':   return new Gps($options[0]);
            case 'Plasma':    return new Plasma($options[0]);
        }
        return false;

    }
}

這時候,戰衣建立之初就可以使用這個工廠!

//鋼鐵俠
class IronMan
{
    //能力
    protected $power;

    function __construct()
    {
        $factory = new ArmorModuleFactory();
        $this->power = $factory->makeModule('Flight', ['10', '20']);
    }
}

可以看得出,我們不再需要和之前一樣去初始化許多第三方類,只需初始化一個工廠類,即可滿足需求。但這樣似乎和以前區別不大,只是沒有那麼多 new關鍵字。其實我們稍微改造一下這個類,你就明白,工廠類的真正意義和價值了。

//鋼鐵俠
class IronMan
{
    //能力
    protected $power;

    function __construct(array $modules)
    {
        $factory = new ArmorModuleFactory();
        foreach ($modules as $moduleName => $moduleOptions){
            $this->power = $factory->makeModule($moduleName,$moduleOptions);
        }

    }
}
//建立鋼鐵俠
$customer = new IronMan([
    'Flight' =>['10', '20'],
    'Gps' =>['8']
]);

現在修改的結果令人滿意。現在,“戰衣” 的建立不再依賴任何一個 “能力” 的類,我們如若修改了或者增加了新的能力屬性,只需要針對修改 ArmorModuleFactory 即可。擴充超能力的同時不再需要重新編輯鋼鐵俠的類檔案,使得我們變得很輕鬆。但是,這才剛剛開始。

再進一步!IoC 容器的重要組成 —— 依賴注入!

由鋼鐵俠依賴戰衣的屬性變成了鋼鐵俠依賴”戰衣屬性工廠”,製作起來更加的得心應手。但是依賴還是並未解除, 如果工廠出現了問題,問題將會變的非常複雜。

其實大多數情況下做到這一步基本已足夠了。

工廠模式的缺點就是:介面未知(即沒有一個很好的契約模型,關於這個我馬上會有解釋)、產生物件型別單一。總之就是,還是不夠靈活。雖然如此,工廠模式依舊十分優秀,並且適用於絕大多數情況。不過我們為了講解後面的 依賴注入 ,這裡就先誇大一下工廠模式的缺陷咯。
我們都知道鋼鐵俠依賴戰衣的模組,我們要求有一個統一介面,這樣才能和鋼鐵俠身上的注入介面對接,最終起到提升戰衣屬性的效果。

現在出現了更加強大又可怕的敵人,以前的能力已經完全跟不上敵人出現的速度了。這時候似乎工廠的生產能力顯得有些不足 —— 由於工廠模式下,所有的模組都已經在工廠類中安排好了,如果有新的、高階的模組加入,我們必須修改工廠類(好比增加新的生產線)。

//戰衣工廠
class ArmorModuleFactory
{
    public function makeModule($moduleName, $options)
    {
        switch ($moduleName) {
            case 'Flight':   return new Flight($options[0], $options[1]);
            case 'Gps':   return new Gps($options[0]);
            case 'Plasma':    return new Plasma($options[0]);
            // case 'more': .......
            // case 'and more': .......
            // case 'and more': .......
            // case 'oh no! its too many!': .......
        }
        return false;
    }
}

又出現了噩夢!!!

其實靈感就差一步!你可能會想到更為靈活的辦法!對,下一步就是我們今天的主要配角 —— DI (依賴注入)

由於需求不斷增大,我們需要集合整個世界的高智商人才,一起解決問題,不應該僅僅只有幾個工廠壟斷負責。不過高智商人才們都非常自負,認為自己的想法是對的,創造出的超能力模組沒有統一的介面,自然而然無法被正常使用。這時我們需要提出一種契約,這樣無論是誰創造出的模組,都符合這樣的介面,自然就可被正常使用。

interface ArmorModuleInterface
{
    /**
     * 戰衣能力啟用方法
     *
     * 任何一個能力都得有該方法,並擁有一個引數
     *@param array $target 針對目標,可以是一個或多個,自己或他人
     */
    public function activate(array $target);
}

定義了一個契約,所有建立的模組都必須遵守規範,才能生產。

其實,這就是 php 中 介面( interface ) 的用處和意義!很多人覺得,為什麼 php 需要介面這種東西?難道不是 java 、 C# 之類的語言才有的嗎?這麼說,只要是一個正常的物件導向程式語言(雖然 php 可以程式導向),都應該具備這一特性。因為一個 物件(object) 本身是由他的模板或者原型 —— 類 (class) ,經過例項化後產生的一個具體事物,而有時候,實現統一種方法且不同功能(或特性)的時候,會存在很多的類(class),這時候就需要有一個契約,讓大家編寫出可以被隨時替換卻不會產生影響的介面。這種由程式語言本身提出的硬性規範,會增加更多優秀的特性。

雖然有些繞,但通過我們接下來的例項,大家會慢慢領會介面帶來的好處

這時候,那些提出更好的超能力模組的高智商人才,遵循這個介面,建立了下述(模組)類:

/**
 * X-超能量
 */
class XPower implements ArmorModuleInterface
{
    public function activate(array $target)
    {
        // 這只是個例子。。具體自行腦補
    }
}

/**
 * 終極炸彈 (就這麼俗)
 */
class UltraBomb implements ArmorModuleInterface
{
    public function activate(array $target)
    {
        // 這只是個例子。。具體自行腦補
    }
}

同時,為了防止有些 “磚家” 自作聰明,或者一些叛徒惡意搗蛋,不遵守契約胡亂製造模組,影響超人,我們對超人初始化的方法進行改造:

//鋼鐵俠
class IronMan
{
    //能力
    protected $module;

    function __construct(ArmorModuleInterface $module)
    {
        $this->module = $module;
    }
}

改造完畢!現在,當我們初始化 “鋼鐵俠” 類的時候,提供的模組例項必須是一個 ArmorModuleInterface 介面的實現。否則就會提示錯誤。
  正是由於鋼鐵俠的創造變得容易,一個鋼鐵俠也就不需要太多的戰衣,我們可以創造多個鋼鐵俠,並分別注入需要的戰衣模組即可。這樣的話,雖然一個鋼鐵俠只有一個戰衣,但鋼鐵俠更容易變多,我們也不怕敵人啦!

現在有人疑惑了,你要講的 依賴注入 呢?
其實,上面講的內容,正是依賴注入。
什麼叫做 依賴注入?

本文從開頭到現在提到的一系列依賴,只要不是由內部生產(比如初始化、建構函式 __construct中通過工廠方法、自行手動 new 的),而是由外部以引數或其他形式注入的,都屬於 依賴注入(DI) 。是不是豁然開朗?事實上,就是這麼簡單。下面就是一個典型的依賴注入:

// 戰衣能力模組
$armorModule = new XPower;
// 初始化一個鋼鐵俠,並注入一個能力模組依賴
$IronMan = new IronMan($armorModule);

關於依賴注入這個本文的主要配角,也就這麼多需要講的。理解了依賴注入,我們就可以繼續深入問題。慢慢走近今天的主角……

更為先進的工廠 —— IoC 容器!

剛剛列了兩段程式碼

// 戰衣能力模組
$armorModule = new XPower;
// 初始化一個鋼鐵俠,並注入一個能力模組依賴
$IronMan = new IronMan($armorModule);

手動建立一個能力組,並且注入這就導致每次都要手動,懶。

現代社會,應該是高效率的生產,乾淨的車間,完美的自動化裝配。

一群敵人來了這樣顯然是不行的,我們需要更加自動化--一條指令,千軍萬馬來相見。我們需要一種高階的生產車間,我們只需要向生產車間提交一個指令碼,工廠便能夠通過指令自動化生產。這種更為高階的工廠,就是工廠模式的昇華 —— IoC 容器。

class Container
{
    protected $binds;

    protected $instances;

    public function bind($abstract, $concrete)
    {
        if ($concrete instanceof Closure) {
            $this->binds[$abstract] = $concrete;
        }else{
            $this->instances[$abstract] = $concrete;
        }

    }

    public function make($abstract, $parameters = [])
    {

        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }
        array_unshift($parameters, $this);

        return call_user_func_array($this->binds[$abstract], $parameters);
    }
}

這時候,一個十分粗糙的容器就誕生了。現在的確很簡陋,但不妨礙我們進一步提升他。先著眼現在,看看這個容器如何使用吧!

// 建立一個容器(後面稱作超級工廠)
$container = new Container;

// 向該 超級工廠 新增 鋼鐵俠 的生產指令碼
$container->bind('IronMan', function($container, $moduleName) {
    return new IronMan($container->make($moduleName));
});

// 向該 超級工廠 新增 能力模組 的生產指令碼
$container->bind('xpower', function($container) {
    return new XPower;
});
////
////// 同上
////$container->bind('ultrabomb', function($container) {
////    return new UltraBomb;
////});

// ******************  華麗麗的分割線  **********************
// 開始啟動生產
$superman_1 = $container->make('IronMan', ['xpower']);
$superman_1->test();
//$superman_2 = $container->make('IronMan', ['ultrabomb']);
//$superman_3 = $container->make('IronMan', ['xpower']);
// ...隨意新增

看到沒?通過最初的 繫結bind 操作,我們向 超級工廠 註冊了一些生產指令碼,這些生產指令碼在生產指令下達之時便會執行。發現沒有?我們徹底的解除了 超人 與 超能力模組 的依賴關係,更重要的是,容器類也絲毫沒有和他們產生任何依賴!我們通過註冊、繫結的方式向容器中新增一段可以被執行的回撥(可以是匿名函式、非匿名函式、類的方法)作為生產一個類的例項的 指令碼 ,只有在真正的 生產make 操作被呼叫執行時,才會觸發。

這樣一種方式,使得我們更容易在建立一個例項的同時解決其依賴關係,並且更加靈活。當有新的需求,只需另外繫結一個“生產指令碼”即可。

實際上,真正的 IoC 容器更為高階。我們現在的例子中,還是需要手動提供超人所需要的模組引數,但真正的 IoC 容器會根據類的依賴需求,自動在註冊、繫結的一堆例項中搜尋符合的依賴需求,並自動注入到建構函式引數中去。Laravel 框架的服務容器正是這麼做的。實現這種功能其實理論上並不麻煩,但我並不會在本文中寫出,因為……我懶得寫。
不過我告訴大家,這種自動搜尋依賴需求的功能,是通過 反射(Reflection) 實現的,恰好的,php 完美的支援反射機制!關於反射,php 官方文件有詳細的資料,並且中文翻譯基本覆蓋,足夠學習和研究!
php.net/manual/zh/book.reflection.p...

原文連結: www.insp.top/learn-laravel-contain... ;

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章