1. 程式人生 > 實用技巧 >深入講解laravel6.0框架中的IOC和DI原理

深入講解laravel6.0框架中的IOC和DI原理

官方解釋:
IOC - 控制反轉 DI - 依賴注入


通俗舉例:
小明以前很窮,風餐露宿,居無定所。現在發財了,自己也想擁有屬於自己的房子,這個時候小明想,要不回老家蓋一棟房子,一來可以住,二來可以光宗耀祖,這個時候,小明需要自己去打造一棟房子;後來小明又想,為何不在城市裡直接買套房呢,生活更加豐富多彩也方便。於是,小明就找了房產中介(IOC容器)買了房子(依賴注入),最終小明很快就住上了屬於自己的房子,開心快樂極了。。。


小明 依賴 房子,小明從自己蓋房子(自己“控制”房子)到找中介買房子(讓中介“控制”房子),這就叫做控制反轉,也就是IOC;而房產中介根據小明的需求,直接把房子提供給小明(當然小明付錢了),這就叫做依賴注入

,也就是DI。


當然,這個房子並不是房產中介建設的,而是開發商建設的,這個開發商就是服務提供者
三十年後,小明的這套房子格局跟不上時代了,住得不舒服,想改造/重新裝修房子,但是時間成本太高了,於是,小明又找房產中介買了房子,小明又很快住上新房子了。。。這也體現了面向物件中類的單一職責原則


目的
採用IOC思想和DI設計模式,主要目的是:解耦
開車式:異地戀。就算中間隔著一個距離,但也不影響真心的相愛著。
原生程式碼實現


傳統寫法

<?php
/**
 * Create by PhpStorm
 * User : Actor
 * Date : 2019-11-01
 * Time : 22:03
 */

/**
 * Class 購房者
 */
class 購房者
{
    private $姓名;

    public function __construct($姓名)
    {
        $this->姓名 = $姓名;
    }

    public function 買房()
    {
        $新房 = new 商品房('0001', '四室兩廳', '180平方米');
        echo '我是'.$this->姓名."\r\n";
        echo '我買了'. $新房->獲取戶型(). '的房子了'."\r\n";
        echo '我買了'. $新房->獲取面積(). '的房子了'."\r\n";
    }

}

/**
 * Class 商品房
 */
class 商品房
{
    private $房源編號;
    private $戶型;
    private $面積;

    public function __construct($房源編號, $戶型, $面積)
    {
        $this->房源編號 = $房源編號;
        $this->戶型 = $戶型;
        $this->面積 = $面積;
    }

    public function 獲取面積()
    {
        return $this->面積;
    }

    public function 獲取戶型()
    {
        return $this->戶型;
    }
}

$大明 = new 購房者('大明');
$大明->買房();
?>

以上程式碼輸出

[actor20:49:55] /projects/phpAdvanced$ php ioc/iocDriveCar.php 
我是大明
我買了四室兩廳的房子了
我買了180平方米的房子了
[actor20:56:18] /projects/phpAdvanced$

採用IOC和DI的思想來實現

<?php
/**
 * Create by PhpStorm
 * User : Actor
 * Date : 2019-11-01
 * Time : 22:03
 */


/**
 * Class 購房者
 */
class 購房者
{
    private $姓名;

    public function __construct($姓名)
    {
        $this->姓名 = $姓名;
    }

    public function 買房(商品房 $新房)
    {
        echo '我是'.$this->姓名."\r\n";
        echo '我買了'. $新房->獲取戶型(). '的房子了'."\r\n";
        echo '我買了'. $新房->獲取面積(). '的房子了'."\r\n";
    }

}

/**
 * Class 商品房
 */
class 商品房
{
    private $房源編號;
    private $戶型;
    private $面積;

    public function __construct($房源編號, $戶型, $面積)
    {
        $this->房源編號 = $房源編號;
        $this->戶型 = $戶型;
        $this->面積 = $面積;
    }

    public function 獲取面積()
    {
        return $this->面積;
    }

    public function 獲取戶型()
    {
        return $this->戶型;
    }
}

/**
 * 房產中介,就是我們講的ioc
 * Class 房產中介
 */
class 房產中介
{
    private $在售房源 = [];//這個類似於laravel的Container物件中的$bindings
    private $認籌房源 = [];//類似於laravel的Container物件中的$resolved
    private $公租房 = [];//類似於laravel的Container物件中的$instances
    private $網紅房源 = [];//類似於laravel的Container物件中的$aliases / $abstractAliases
    private $意向購房群體 = [];

    public function 預售登記($戶型, $詳細資訊)
    {
        $this->在售房源[$戶型] = $詳細資訊;
    }

    public function 獲取在售房源($戶型)
    {
        return ($this->在售房源[$戶型])();//因為是閉包,所以,要增加()來執行閉包函式
    }

    public function 意向登記($意向人, $個人資訊)
    {
        $this->意向購房群體[$意向人] = $個人資訊;
    }

    public function 獲取意向人資訊($意向人)
    {
        return ($this->意向購房群體[$意向人])();//因為是閉包,所以,要增加()來執行閉包函式
    }
}

$app = new 房產中介();
$app->預售登記('三室一廳', function(){
    return new 商品房('1001', '三室一廳', '100平方米');
});
$app->預售登記('四室兩廳', function(){
    return new 商品房('1002', '四室兩廳', '150平方米');
});

$app->意向登記('小明', function(){
    return new 購房者('小明');
});

$app->意向登記('張三', function(){
    return new 購房者('張三');
});

//echo $app->獲取意向人資訊('小明')->買房($app->獲取在售房源('四室兩廳'));

$意向人 = $app->獲取意向人資訊('小明');
$新房 = $app->獲取在售房源('四室兩廳');
$意向人->買房($新房);
?>

以上程式輸出

[actor20:49:43] /projects/phpAdvanced$ php ioc/iocDriveCar.php 
我是小明
我買了四室兩廳的房子了
我買了150平方米的房子了
[actor20:49:55] /projects/phpAdvanced$

對IOC和DI的本質分析


從上面的程式碼,我們看到,房產中介作為IOC,其實本質就是陣列(可以是一維陣列,也可以是多維陣列)。


其實,在laravel框架中,Container物件中的屬性$bindings$resolved$instances$aliases$abstractAliases其實也是從不同維度來管理註冊到容器中的物件的。


上面的例子中,如果從業務邏輯角度來講,無非就是購房者要買房,主要的類有:購房者、商品房。


如果按照傳統程式碼來實現,那麼購房者物件對商品房物件的依賴是強依賴,因為在購房者類中需要new 商品房()


而在採用IOC和DI的思想來實現的話,增加了房產中介物件這個IOC容器,商品房首先在房產中介那邊進行一下預售登記,購房者也在房產中介那邊進行一下意向登記,購房者物件需要依賴商品房物件,採用依賴注入,即:讓房產中介直接把例項化後到商品房物件注入到購房者物件,購房者物件無需關注怎麼例項化,只管拿過來用就行。


Laravel框架IOC核心原始碼——繫結
我們來簡單看下Laravel框架的核心容器的繫結是怎麼實現的?


以下程式碼都是在Illuminate\Container\Container類中

  1. 通過instance方法繫結
/**
        * Register an existing instance as shared in the container.
        *
        * @param  string  $abstract
        * @param  mixed   $instance
        * @return mixed
        */
       public function instance($abstract, $instance)
       {
           $this->removeAbstractAlias($abstract);
   
           $isBound = $this->bound($abstract);
   
           unset($this->aliases[$abstract]);
   
           // We'll check to determine if this type has been bound before, and if it has
           // we will fire the rebound callbacks registered with the container and it
           // can be updated with consuming classes that have gotten resolved here.
           $this->instances[$abstract] = $instance;
   
           if ($isBound) {
               $this->rebound($abstract);
           }
   
           return $instance;
       }

使用該方法註冊繫結到容器中的物件例項是共享的。

2.bind方法

/**
       * Register a binding with the container.
       *
       * @param  string  $abstract
       * @param  \Closure|string|null  $concrete
       * @param  bool  $shared
       * @return void
       */
      public function bind($abstract, $concrete = null, $shared = false)
      {
          $this->dropStaleInstances($abstract);
  
          // If no concrete type was given, we will simply set the concrete type to the
          // abstract type. After that, the concrete type to be registered as shared
          // without being forced to state their classes in both of the parameters.
          if (is_null($concrete)) {
              $concrete = $abstract;
          }
  
          // If the factory is not a Closure, it means it is just a class name which is
          // bound into this container to the abstract type and we will just wrap it
          // up inside its own Closure to give us more convenience when extending.
          if (! $concrete instanceof Closure) {
              $concrete = $this->getClosure($abstract, $concrete);
          }
  
          $this->bindings[$abstract] = compact('concrete', 'shared');
  
          // If the abstract type was already resolved in this container we'll fire the
          // rebound listener so that any objects which have already gotten resolved
          // can have their copy of the object updated via the listener callbacks.
          if ($this->resolved($abstract)) {
              $this->rebound($abstract);
          }
      }

如何使用bind方法來將物件註冊繫結到容器中呢?如下,bind方法是將閉包繫結到容器當中

$this->app->bind('User\API', function ($app) {
        return new User\API($app->make('UserLogin'));
  });

同樣,bind方法會先刪除舊的例項,然後再新的例項放入閉包中,再繫結到容器中。如果第二個引數不是閉包,會通過getClosure方法將類名封裝到閉包中,然後在閉包中通過make方法或build方法解析繫結的類。繫結時會將閉包和是否是shared放入$this->bind[]陣列中,解析時呼叫。

singleton方法

/**
       * Register a shared binding in the container.
       *
       * @param  string  $abstract
       * @param  \Closure|string|null  $concrete
       * @return void
       */
      public function singleton($abstract, $concrete = null)
      {
          $this->bind($abstract, $concrete, true);
      }

從官方的程式碼可以看出,singleton方法,最終還是呼叫了$this->bind()方法,只是通過singleton()方法繫結到容器的物件只會被解析一次,之後的呼叫都返回相同的例項,這就是所謂的單例。


Laravel框架IOC核心原始碼——解析

看原始碼

    /**
     * Resolve the given type from the container.
     *
     * @param  string  $abstract
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    public function make($abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }






    /**
     * Resolve the given type from the container.
     *
     * @param  string  $abstract
     * @param  array  $parameters
     * @param  bool   $raiseEvents
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    protected function resolve($abstract, $parameters = [], $raiseEvents = true)
    {
        $abstract = $this->getAlias($abstract);

        $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );

        // If an instance of the type is currently being managed as a singleton we'll
        // just return an existing instance instead of instantiating new instances
        // so the developer can keep using the same objects instance every time.
        if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

        $this->with[] = $parameters;

        $concrete = $this->getConcrete($abstract);

        // We're ready to instantiate an instance of the concrete type registered for
        // the binding. This will instantiate the types, as well as resolve any of
        // its "nested" dependencies recursively until all have gotten resolved.
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        // If we defined any extenders for this type, we'll need to spin through them
        // and apply them to the object being built. This allows for the extension
        // of services, such as changing configuration or decorating the object.
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }

        // If the requested type is registered as a singleton we'll want to cache off
        // the instances in "memory" so we can return it later without creating an
        // entirely new instance of an object on each subsequent request for it.
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }

        if ($raiseEvents) {
            $this->fireResolvingCallbacks($abstract, $object);
        }

        // Before returning, we will also set the resolved flag to "true" and pop off
        // the parameter overrides for this build. After those two things are done
        // we will be ready to return back the fully constructed class instance.
        $this->resolved[$abstract] = true;

        array_pop($this->with);

        return $object;
    }




    /**
     * Instantiate a concrete instance of the given type.
     *
     * @param  string  $concrete
     * @return mixed
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    public function build($concrete)
    {
        // If the concrete type is actually a Closure, we will just execute it and
        // hand back the results of the functions, which allows functions to be
        // used as resolvers for more fine-tuned resolution of these objects.
        if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }

        try {
            $reflector = new ReflectionClass($concrete);
        } catch (ReflectionException $e) {
            throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
        }

        // If the type is not instantiable, the developer is attempting to resolve
        // an abstract type such as an Interface or Abstract Class and there is
        // no binding registered for the abstractions so we need to bail out.
        if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

        $this->buildStack[] = $concrete;

        $constructor = $reflector->getConstructor();

        // If there are no constructors, that means there are no dependencies then
        // we can just resolve the instances of the objects right away, without
        // resolving any other types or dependencies out of these containers.
        if (is_null($constructor)) {
            array_pop($this->buildStack);

            return new $concrete;
        }

        $dependencies = $constructor->getParameters();

        // Once we have all the constructor's parameters we can create each of the
        // dependency instances and then use the reflection instances to make a
        // new instance of this class, injecting the created dependencies in.
        try {
            $instances = $this->resolveDependencies($dependencies);
        } catch (BindingResolutionException $e) {
            array_pop($this->buildStack);

            throw $e;
        }

        array_pop($this->buildStack);

        return $reflector->newInstanceArgs($instances);
    }

從上面的第三段程式碼build()方法中可以看出,解析時,如果繫結註冊的是閉包函式,那麼就是直接返回閉包函式的執行,關鍵程式碼如下

if ($concrete instanceof Closure) {
            return $concrete($this, $this->getLastParameterOverride());
        }

如果繫結註冊的是類名,那麼就利用php的反射(ReflectionClass)來例項化物件,並返回給呼叫者。bind()方法剩下的程式碼乾的就是這個工作。

最後,在resolve()方法中的

$this->resolved[$abstract] = true;

代表已經解析成功了。

對PHP後端技術,對PHP架構技術感興趣的朋友,我的官方群點選此處,一起學習,相互討論。

PHP進階學習思維導圖、面試;文件、視訊資源免費獲取​shimo.im

群內已經有管理將知識體系整理好(原始碼,學習視訊等資料),歡迎加群免費領取。

部分資料截圖: