开车式讲解laravel框架中的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;
       }

使用该方法注册绑定到容器中的对象实例是共享的。

  1. 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[]数组中,解析时调用。

  1. 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;

代表已经解析成功了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345