1. 程式人生 > >laravel服務容器-----深入理解控制反轉(IoC)和依賴注入(DI)

laravel服務容器-----深入理解控制反轉(IoC)和依賴注入(DI)

首先大家想一想什麼是容器,字面意思就是盛放東西的東西,常見的變數,物件屬性都是容器,一個容器能夠裝什麼東西,完全在於你對這個容器的定義。有的容器不僅僅只是存文字,變數,而是物件,屬性,那麼我們通過這種容器就可以進行很多高階的功能。

IoC容器

IoC容器是laravel的核心,它提供了框架需要的一系列的服務,文件上稱他為服務容器,解釋為一個用於管理類依賴和執行依賴注入的強大工具,聽起來暈暈乎乎的,之後在文件中找到一片文章:http://laravelacademy.org/post/769.html。我寫這篇文章也是為了更好的理解什麼 是控制反轉和依賴注入。

依賴的產生----超人和超能力

在面向物件的程式設計中,總要接觸幾個東西:介面,類,物件。介面是類的原型,一個類一定要遵守實現的介面,物件則是一個類例項化的產物。這樣子說不利於理解,我們可以舉一個例子。

  •   一個怪物橫行的世界裡,我們需要超人來戰勝怪獸,維護世界和平。

我們把超人當做一個類: 

class SuperMan {}

我們可以想一下,一個超人誕生的時候肯定有至少有一個超能力,我們可以把這個超能力也抽象為一個物件,一個超能力肯定有多種屬性,以及操作方法,我們先大致定義一個只有屬性的類,至於方法,我們可以到後面進行補充:

class Power {
 
    protected $ability;  //能力值
    protected $range;   //能力範圍
     
    public function ____construct($ability,$range) {
        $this->ability = $ability;
        $this->range = $range;
    }
}

  這時我們可以回去修改一下超人類,讓在超人被建立的時候賦予一個超能力:

class SuperMan
{
    protected $power:
     
    public function __construct() {
         $this->power = new Power(999,999);
    }
}

  這樣子,超人在建立的時候就有了超能力,我們這個時候可以發現,超人 和超能力之間現在存在一種依賴,怎麼體現出這個依賴了呢,所謂依賴,就是我若依賴了你,那我就離不開你。在面向物件的程式設計中,這種依賴隨處可見,少量的依賴並不影響什麼,但是當在一個專案中,這種依賴達到一種量級的時候,那會舉步維艱!

之前的例子中,超能力類例項化後是一個具體的超能力,但是我們知道,超人的超能力是多元化的,每種超能力的方法、屬性都有不小的差異,沒法通過一種類描述完全。我們現在進行修改,我們假設超人可以有以下多種超能力:

  • 飛行,屬性有:飛行速度、持續飛行時間
  • 蠻力,屬性有:力量值
  • 能量彈,屬性有:傷害值、射擊距離、同時射擊個數

我們會建立這些類:

class Flight
{
    protected $speed;
    protected $holdtime;
    public function __construct($speed, $holdtime) {}
}
 
class Force
{
    protected $force;
    public function __construct($force) {}
}
 
class Shot
{
    protected $atk;
    protected $range;
    protected $limit;
    public function __construct($atk, $range, $limit) {}
}

  那麼此時,超人在建立的時候,我們會根據需要來進行初始化超能力,如下:

class Superman
{
    protected $power;
 
    public function __construct()
    {
        $this->power = new Fight(9, 100);
        // $this->power = new Force(45);
        // $this->power = new Shot(99, 50, 2);
        /*
        $this->power = array(
            new Force(45),
            new Shot(99, 50, 2)
        );
        */
    }
}

  我們需要手動在建構函式或者其他方法中例項化一系列的類,這樣子真的好嗎?可以想想,當不同的怪獸侵略地球,我們需要不同超能力的超人更好的打敗怪獸時,我們血藥重新改造超人,換句話說,改變超能力的同時,我們還需要重新建立一個超人。這樣子的效率是不是太低了呢,我們還有什麼更好的方法呢?

看過鋼鐵俠的人都知道鋼鐵俠可以隨時更換自己的智慧晶片,那麼我們是不是可以讓超人的超能力可以隨時更換,只需要更換一種裝置或者晶片呢?之前我們是在超人類中固化了他的超能力初始化的行為,如果我們要實現通過更換晶片來改變超人的超能力,那麼就應該將超能力初始化行為轉為由外部進行,由外部建立超能力模組,然後植入超人體內一個特定的介面,只要那些超能力模組能夠滿足這個介面都可以被超人利用,增大超人的能力。這種由外部負責其依賴需求的行為,我們可以稱其為 “控制反轉(IoC)”。

工廠模式,依賴轉移!

在實現控制反轉前,我們可以思考思考:我們可以想到,元件、工具(或者超人的模組),是一種可被生產的玩意兒,生產的地方當然是 “工廠(Factory)”,於是有人就提出了這樣一種模式: 工廠模式。

工廠模式,顧名思義,就是一個類所依賴的外部事物的例項,都可以被一個或多個 “工廠” 建立的這樣一種開發模式,就是 “工廠模式”。

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

class SuperModuleFactory
{
     public function makeModule($moduleName, $options) {
          switch ($moduleName) {
            case 'Fight':
                return new Fight($options[0], $options[1]);
            case 'Force':
                return new Force($options[0]);
            case 'Shot':
                return new Shot($options[0], $options[1], $options[2]);
        }
     }
}

這時候,超人 建立之初就可以使用這個工廠:

class Superman
{
    protected $power;
 
    public function __construct()
    {
        // 初始化工廠
        $factory = new SuperModuleFactory;
 
        // 通過工廠提供的方法制造需要的模組
        $this->power = $factory->makeModule('Fight', [9, 100]);
        // $this->power = $factory->makeModule('Force', [45]);
        // $this->power = $factory->makeModule('Shot', [99, 50, 2]);
        /*
        $this->power = array(
            $factory->makeModule('Force', [45]),
            $factory->makeModule('Shot', [99, 50, 2])
        );
        */
    }
}

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

class Superman
{
    protected $power;
 
    public function __construct(array $modules)
    {
        // 初始化工廠
        $factory = new SuperModuleFactory;
 
        // 通過工廠提供的方法制造需要的模組
        foreach ($modules as $moduleName => $moduleOptions) {
            $this->power[] = $factory->makeModule($moduleName, $moduleOptions);
        }
    }
}
 
// 建立超人
$superman = new Superman([
    'Fight' => [9, 100],
    'Shot' => [99, 50, 2]
]);

IoC 容器的重要組成 —— 依賴注入  現在,“超人” 的建立不再依賴任何一個 “超能力” 的類,我們如若修改了或者增加了新的超能力,只需要針對修改 SuperModuleFactory 即可。擴充超能力的同時不再需要重新編輯超人的類檔案,使得我們變得很輕鬆。但是,這才剛剛開始。 

由 “超人” 對 “超能力” 的依賴變成 “超人” 對 “超能力模組工廠” 的依賴後,對付小怪獸們變得更加得心應手。但這也正如你所看到的,依賴並未解除,只是由原來對多個外部的依賴變成了對一個 “工廠” 的依賴。假如工廠出了點麻煩,問題變得就很棘手。其實大多數情況下,工廠模式已經足夠了。工廠模式的缺點就是:介面未知(即沒有一個很好的契約模型,關於這個我馬上會有解釋)、產生物件型別單一。總之就是,還是不夠靈活。雖然如此,工廠模式依舊十分優秀,並且適用於絕大多數情況。

我們知道,超人依賴的模組,我們要求有統一的介面,這樣才能和超人身上的注入介面對接,最終起到提升超能力的效果。其實侵略地球的不僅僅只有這一點小怪獸,後面還有很多大怪獸,這個時候我們的工廠好像有點力不從心了,因為每次要新填一個超能力,總是要對工廠進行維修和翻新,相當於增加生產線。是不是感覺還是很是噩夢,下一步就是我們今天的主要配角 —— DI (依賴注入)

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

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

 上文中,我們定下了一個超能力介面(模組規範),所有被建立的模組必須遵守這個契約才能被生產和使用。

可能大家對於在php中出現介面這種東西感到很是驚奇,這種東西不是隻有在java,c++中才有的嗎,其實對於任何一款面向物件的程式語言,介面都是存在並且有很大作用的。因為一個物件,都是由他的模板或者原型---類經過例項化後的一個具體事物,有時候,要實現統一方法但具有不同功能或特性的時候,需要很多類,這個時候就需要一個契約,讓大家編寫出可以隨時替換但又不會產生影響的介面,這種由程式語言本身提出的硬性規範,會增加更多優秀的特性。大家之後就會懂得介面的好處。

在這個模組被提出來之後,高智商的科學家們遵循這個介面,建立了很多類:

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

  同時,為了防止一些科學家不遵守這個介面進行惡意的搗亂,我們現在要修改之前的超人類:

class Superman
{
    protected $module;
 
    public function __construct(SuperModuleInterface $module)
    {
        $this->module = $module;
    }
}

改造完畢!現在,當我們初始化 “超人” 類的時候,提供的模組例項必須是一個 SuperModuleInterface 介面的實現。否則就會提示錯誤。

正是由於超人的創造變得容易,一個超人也就不需要太多的超能力,我們可以創造多個超人,並分別注入需要的超能力模組即可。這樣的話,雖然一個超人只有一個超能力,但超人更容易變多,我們也不怕怪獸啦!

現在有人疑惑了,你要講的依賴注入呢?

其實,上面講的內容,正是依賴注入。

什麼叫做依賴注入?

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

// 超能力模組
$superModule = new XPower;
// 初始化一個超人,並注入一個超能力模組依賴
$superMan = new Superman($superModule);

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

剛剛列了一串程式碼:

// 超能力模組
$superModule = new XPower;
// 初始化一個超人,並注入一個超能力模組依賴
$superMan = new Superman($superModule);

  現在我們能發現,這是手動的建立一個超人和超能力模板,並且將模板注入剛剛建立的超人當中,然而,現代社會,應該是高效率的生產,乾淨的車間,完美的自動化裝配!

我們需要自動化 —— 最多一條指令,千軍萬馬來相見。我們需要一種高階的生產車間,我們只需要向生產車間提交一個指令碼,工廠便能夠通過指令自動化生產。這樣子,當一大堆怪獸來了我們只需要一條指令就有千軍萬馬來相見。這種更為高階的工廠,就是工廠模式的昇華 —— 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('superman', function($container, $moduleName) {
    return new Superman($container->make($moduleName));
});
 
// 向該 超級工廠新增超能力模組的生產指令碼
$container->bind('xpower', function($container) {
    return new XPower;
});
 
// 同上
$container->bind('ultrabomb', function($container) {
    return new UltraBomb;
});
 
// ****************** 華麗麗的分割線 **********************
// 開始啟動生產
$superman_1 = $container->make('superman', 'xpower');
$superman_2 = $container->make('superman', 'ultrabomb');
$superman_3 = $container->make('superman', 'xpower');
// ...隨意新增

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

實際上,真正的 IoC 容器更為高階。我們現在的例子中,還是需要手動提供超人所需要的模組引數,但真正的 IoC 容器會根據類的依賴需求,自動在註冊、繫結的一堆例項中搜尋符合的依賴需求,並自動注入到建構函式引數中去。Laravel 框架的服務容器正是這麼做的。實現這種功能其實理論上並不麻煩。

現在,到目前為止,我們已經不再懼怕怪獸們了。高智商人才集思廣益,井井有條,根據介面契約創造規範的超能力模組。超人開始批量產出。最終,人人都是超人,你也可以是哦!

重新審視 Laravel 的核心

現在,我們開始慢慢解讀 Laravel 的核心。其實,Laravel 的核心就是一個 IoC 容器,也恰好是我之前所說的高階的 IoC 容器。

可以說,Laravel 的核心本身十分輕量,並沒有什麼很神奇很實質性的應用功能。很多人用到的各種功能模組比如 Route(路由)、Eloquent ORM(資料庫 ORM 元件)、Request(請求)以及 Response(響應)等等等等,實際上都是與核心無關的類模組提供的,這些類從註冊到例項化,最終被你所使用,其實都是 Laravel 的服務容器負責的。

我們以大家最常見的 Route 類作為例子。大家可能經常見到路由定義是這樣的:

Route::get('/', function() {
    // bla bla bla...
});

實際Route 類被定義在這個名稱空間:Illuminate\Routing\Router

檔案 vendor/laravel/framework/src/Illuminate/Routing/Router.php,

服務提供者

我們在前文介紹 IoC 容器的部分中,提到了,一個類需要繫結、註冊至容器中,才能被“製造”。

對,一個類要被容器所能夠提取,必須要先註冊至這個容器。既然 Laravel 稱這個容器叫做服務容器,那麼我們需要某個服務,就得先註冊、繫結這個服務到容器,那麼提供服務並繫結服務至容器的東西,就是服務提供者(Service Provider)。

雖然,繫結一個類到容器不一定非要通過服務提供者。

但是,我們知道,有時候我們的類、模組會有需要其他類和元件的情況,為了保證初始化階段不會出現所需要的模組和元件沒有註冊的情況,Laravel 將註冊和初始化行為進行拆分,註冊的時候就只能註冊,初始化的時候就是初始化。拆分後的產物就是現在的服務提供者。

服務提供者主要分為兩個部分,register(註冊) 和 boot(引導、初始化),具體參考文件register 負責進行向容器註冊“指令碼”,但要注意註冊部分不要有對未知事物的依賴,如果有,就要移步至 boot 部分。

我們現在解答之前關於 Route 的方法為何能以靜態方法訪問的問題。實際上這個問題文件上有寫,簡單說來就是模擬一個類,提供一個靜態魔術方法__callStatic,並將該靜態方法對映到真正的方法上。

我們使用的 Route 類實際上是 Illuminate\Support\Facades\Route 通過 class_alias() 函式創造的別名而已,這個類被定義在檔案 vendor/laravel/framework/src/Illuminate/Support/Facades/Route.php 。

我們開啟檔案一看……誒?怎麼只有這麼簡單的一段程式碼呢?

<?php
    namespace Illuminate\Support\Facades;
 
    /**
     * @see \Illuminate\Routing\Router
     */
    class Route extends Facade {
 
        /**
         * Get the registered name of the component.
         *
         * @return string
         */
        protected static function getFacadeAccessor()
        {
            return 'router';
        }
 
}

其實仔細看,會發現這個類繼承了一個叫做 Facade 的類,到這裡謎底差不多要解開了。

上述簡單的定義中,我們看到了 getFacadeAccessor 方法返回了一個 route,這是什麼意思呢?事實上,這個值被一個 ServiceProvider 註冊過,大家應該知道註冊了個什麼,當然是那個真正的路由類!

本文來自:http://laravelacademy.org/post/769.html