深入laravel框架中的IOC和DI原理

255 阅读7分钟

官方解释:
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大厂PDF,PHP进阶架构视频资料,PHP精彩好文可以微信搜索关注:PHP开源社区

2021金三银四大厂面试真题集锦,必看!

四年精华PHP技术文章整理合集——PHP框架篇

四年精华PHP技术文合集——微服务架构篇

四年精华PHP技术文合集——分布式架构篇

四年精华PHP技术文合集——高并发场景篇

四年精华PHP技术文章整理合集——数据库篇