深入Laravel服務容器

JayChen發表於2019-02-16

在開始之前要明確一個概念,不管是設計模式,還是依賴注入等等,都是為了實現模組化.所謂模組化就是希望一個軟體是由很多子模組組成的,這些模組之間的依賴程度儘量的低,也就是如果系統中不需要某一個功能,那麼只要移除這個功能所對應的模組就可以了.

那麼,我們今天要說的服務容器就是為了實現上面的功能.你應該聽過,Laravel中的服務容器其本質上是一個IoC容器,但是好像隊IoC又不是很瞭解,講來講去優點很多,功能很強勁.但是不懂原理怎麼用都不踏實啊.所以,這裡我們自己來實現一個IoC容器,洞察其本質.

在開始之前,先說明一點,閱讀本篇文章至少要保證有一下的基礎知識:

  • php反射用法

  • 閉包的use用法

如果不懂上面的內容,請先補充.避免閱讀程式碼時候產生的不適感.

<?php

/**
 * Created by PhpStorm.
 * User: jiayao
 * Date: 2016/9/1
 * Time: 21:41
 */


class Container
{
    public $binding = [];

                   
    /**
     * @param $abstract
     * @param null $concrete
     * @param bool $shared  
     */
    public function bind($abstract, $concrete = null, $shared = false)
    {
        if (!$concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->binding[$abstract] = compact(`concrete`, `shared`);
    }



    protected function getClosure($abstract, $concrete)
    {
        return function ($c) use ($abstract, $concrete) {
            $method = ($abstract == $concrete) ? `build` : `make`;
            return $c->$method($concrete);
        };
    }

    /**
     * @param $abstract
     * @return object
     *
     */
    public function make($abstract)
    {
        $concrete = $this->getConcrete($abstract);

        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }
        return $object;
    }

    /**
     * @param $concrete
     * @param $abstract
     * @return bool
     */
    public function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract || $concrete instanceof Closure;
    }

    /**
     * @param $abstract
     * @return mixed
     */
    protected function getConcrete($abstract)
    {
        if (!isset($this->binding[$abstract])) {
            return $abstract;
        }
        return $this->binding[$abstract][`concrete`];
    }


    /**
     * @param $concrete
     * @return object
     */
    public function build($concrete) {

        if($concrete instanceof Closure) {
            return $concrete($this);
        }
        
        //反射...
        $reflector = new ReflectionClass($concrete);
        if(!$reflector->isInstantiable()) {
            echo $message = "Target [$concrete] is not instantiable";
        }
        //獲取要例項化物件的建構函式
        $constructor = $reflector->getConstructor();
        
        //沒有定義建構函式,只有預設的建構函式,說明建構函式引數個數為空
        if(is_null($constructor)) {
            return new $concrete;
        }

        //獲取建構函式所需要的所有引數
        $dependencies = $constructor->getParameters();
        $instances = $this->getDependencies($dependencies);
        
                            //從給出的陣列引數在中例項化物件
        return $reflector->newInstanceArgs($instances);
    }

    /**
     * @param $paramters
     * @return array
     * 獲取構建類所需要的所有依賴,級建構函式所需要的引數 ,
     */
    protected function getDependencies($paramters) {
        $dependencies = [];
        
        foreach ($paramters as $paramter) {
            //獲取到引數名稱.
            $dep = $paramter->getClass();
            if(is_null($dep)){
                $dependencies = null;
            }else{
                $dependencies[] = $this->resolveClass($paramter);
            }
        }
        return (array)$dependencies;
    }

    /**
     * @param ReflectionParameter $parameter
     * @return object
     * 例項化 建構函式中所需要的引數.
     */
    protected function resolveClass(ReflectionParameter $parameter) {
        $name = $parameter->getClass()->name;
        return $this->make($name);
    }
    
}

這就是一個IoC容器的實現程式碼.乍一看,很麻煩.其實真的蠻麻煩的 =_=,如果是第一次接觸的話,並不是那麼好消化,這裡再給出使用IoC容器的程式碼

<?php

/**
 * Created by PhpStorm.
 * User: jiayao
 * Date: 2016/9/1
 * Time: 21:37
 */
require __DIR__ . `/Container.php`;


interface TrafficTool
{
    public function go();
}

class Train implements TrafficTool
{

    public function go()
    {
        echo "train....";
    }
}

class Leg implements TrafficTool
{
    public function go()
    {
        echo "leg..";
    }
}

class Traveller
{
    /**
     * @var Leg|null|Train
     * 旅行工具
     */
    protected $_trafficTool;

    public function __construct(TrafficTool$trafficTool)
    {
        $this->_trafficTool = $trafficTool;
    }

    public function visitTibet()
    {
        $this->_trafficTool->go();
    }
}

//例項化IoC容器
$app = new Container();

//繫結某一功能到IoC
$app->bind(`TrafficTool`, `Train`);
$app->bind(`travellerA`, `Traveller`);

// 例項化物件
$tra = $app->make(`travellerA`);
$tra->visitTibet();

執行例子發現會輸出:train...這個例子假設旅行者去青藏旅行,可以坐火車(train)或者走路(leg)去青藏.

好了,其實這樣子本篇文章就可以結束了,因為所有的答案都在IoC容器的實現中, 但是為了可以更好的理解上面的程式碼,我們繼續往下分析.

首先,希望你可以執行一下上面的程式碼,雖然簡單的執行程式碼並不會幫助你理解程式碼,但是一個可以執行的例子會讓人比較踏實,能夠更有把握的理解程式碼.

在深入每一行程式碼之前,我們從整體上來分析,IoC解決了一個什麼問題?簡單點說,就是我們再例項化物件的時候不用使用new了,有了IoC容器之後,我們呼叫make函式就可以例項化出一個物件了.然而,你發現,Traveller的建構函式是需要一個引數的,可是我們好像並沒有提供這個引數?

這就是IoC強大之處了, 呼叫make例項化物件的時候,容器會使用反射功能,去分析我們要例項化物件的建構函式,獲取建構函式所需的每個引數,然後分別去例項化這些引數,如果例項化這些引數也要引數,那麼就再去例項化引數的引數…..=_=.到最後成功例項化我們所需要的traveller了.在Container的build函式就是使用反射來例項化物件.

但是,有一個問題了,IoC容器怎麼知道例項化Traveller的時候需要的引數train,而不是leg?

其實,IoC容器什麼都不知道,IoC會例項化哪些物件都是通過bind函式告訴IoC的,上面的例子兩次呼叫bind函式,就是告訴Ioc可以例項化的物件有TrainTraveller. 再通俗講就是:當需要當我們需要TrafficTool這個服務的時候去例項化Train這個類,需要一個travellerA的旅行者的時候去例項化Traveller類.而Train這個就是travellerA就是去青藏的方式. 這樣子如果想要走路去青藏的話只要把$app->bind(`Visit`, `Train`);改為$app->bind(`Visit`, `Leg`);就可以.

可是,這上面的這些有什麼意義?直接$tra = new Traveller($trafficTool)來例項化物件好像也沒有什麼不好的.

使用new來例項化物件的時候,會產生依賴.比如上面$tra = new Traveller($trafficTool),這說明我們要建立一個Traveller之前得有一個$trafficTool,即Traveller依賴於trafficTool.當使用new來例項化Traveller的時候,TravellertrafficTool之間就產生了耦合.這樣,這兩個元件就沒辦法分開了.

而使用IoC是怎麼解決這個問題的,之前說過,如果想要如果想要走路去青藏的話只要把$app->bind(`Visit`, `Train`);改為$app->bind(`Visit`, `Leg`);就可以.這樣子,使用何種方式去青藏,我們可以自由的選擇.

我們站在Laravel框架設計者的角度去想,設計者肯定希望一個框架提供的功能越多越好,但是又要保證強大的同時又不會限制使用者.最好可以保證使用者想實現什麼奇怪的需求都可以.那麼功能強大但是又不侷限的最好方法就是什麼都不做,提供一個強大的IoC容器.所有需要實現的功能都變成一個個服務,需要什麼服務就把服務註冊(即呼叫bind函式)到IoC中,然後讓IoC去管理依賴.

開發者想到一個變態的需求:走路去青藏,那麼只要你實現了走路去青藏這個功能,然後把這個功能當做一個服務註冊到IoC中,以後你需要這個服務的時候IoC就幫你例項化這個服務.當開發者回歸正常之後覺得還是坐火車去吧,於是不註冊走路這個功能,實現坐火車的功能,然後註冊這個功能.下次IoC例項化的時候就是例項化坐火車這個功能了.

好了,剩下的部分就是一行一行的閱讀Container的程式碼了,Laravel框架中的服務容器程式碼也是這個樣子,只是功能更加強悍.但是核心是一樣的,上面的程式碼懂了以後再使用Laravel框架就會更加遊刃有餘了.

文章雖短.但是內容很多.尤其是程式碼,雖然可能只是短短的一個例子,但是包含了很多內容.值得好好分析,這裡放個彩蛋:Traveller中建構函式引數類似為TrafficTool,是一個介面.但是例項化的是Train.這裡體現了設計模式的一個原則

面對介面程式設計,而不是面對實現程式設計.

相關文章