阅读 1205

对PHP框架中的容器的理解

最近在用Laravel框架进行开发,感觉Laravel最大的设计特点就是使用了容器来动态的实现类的加载。说实话,之前我自己并没有接触过容器这么"高大上"的概念,最近一接触第一感觉是‘晕’,不过慢慢理解下来,感觉还是蛮有意思的,下面是我的一点理解。(PS:本文是我个人阅读了 www.hellokang.net/2016/11/13/… 这篇文章后总结的,建议有兴趣的同学可以去看看,写的非常的好!!!)

1:如何解决一个类依赖另外一个类的问题

最简单常见的做法是,我们直接在类的内部引入该类所依赖的类,并对其进行实例化操作,下面是示例代码。

    class A
    {
       public $name = "";
       public function __construct($name)
       {
          $this->name = $name;
          echo '我是被依赖的类...';
       }

    }

    class B
    {
      public function __construct()
      {
         echo '我依赖于类B...';
         require "A类的类文件";
         $Obj = new A("aaa");
      }
    }复制代码

上面的代码简单吧,我们没用几行代码就解决了刚才提到的问题,相信很多人一开始写代码的时候都会这么写的。可是这种写法存在一个很大的问题,就是当B类发生变动的时候,我们需要手动的去A类中进行修改,也就是说这种写法将A类和B类耦合在了一起,不利于程序以后的扩展与维护。

2:如何解决上面所说的代码耦合的问题

既然上面的代码存在缺点,那我们应该怎么解决这个问题那?大家也一定听说过另外一个"高大上"的名词"依赖注入"吧,下面我们就通过依赖注入的方式解决上面的提出的问题,下面看代码。

   //定义一个接口
   interface A_Parent
   {
       function __construct()
       {
       }

       function test()
       {
          //pass
       }
   }

   //B类所依赖的A类通过实现A_Parent这个接口来实现
   class A implements A_Parent
   {
       public $name = "";
       public function  __construct($name)
       {
           $this->name = $name;
       }
       public function test()
       {
          //pass
       }
   }

   //重写B类
   class B
   {
      public function __construct(A_Parent A)
      {
          $Obj = A;
      }
   }

`复制代码

上面的十几行代码,我们就实现了"高大上"的依赖注入了。对于刚刚接触上面代码的同学,可能理解起来有点困难,这里我解释一下。B类依赖于A类,我们之前的做法是在B类中引入A类,并且进行实例化的操作。现在我们不在B类中直接引入A类并实例化了,而是通过B类的构造函数将实例化好的A类对象通过参数的形式传递给B类,这样我们就不需要在B类中写引入或者实例化的代码了,因为我们传递过去的就是一个对象嘛,所以就降低了对A类的依赖。可能有的同学对 A_Parent A这个形参的写法感觉到奇怪,毕竟在PHP传统的编程中很少会用到这种写法。这种 类型 + 参数定义形参的方式,可以提供类型约束。上面的代码中,如果传递过来的参数必须是 A_Parent类型的,正是因为有了这种参数类型的限制,才能保证我们代码的可扩展性。大家想想,如果B类的逻辑发生变化,其现在不需要依赖A类了,而是需要与A类方法相同的另外一个类,那我们只需要实现A_Parent接口生成另外一个类实例化作为参数即可,是不是很方便。

3:什么是容器,具体需要怎么实现

上面的代码我们实现了依赖注入,解决了类依赖类实现的代码耦合的问题。上面的代码看起来的可用了,但是在一个比较大的项目中,类往往是非常多的,类与类之间的依赖关系也是非常的复杂的,如果我们向上面一样,一一手动的来完成依赖的注入,也是非常麻烦的一件事情。可以想象一个,当你要使用B类的时候,你必须需要手动的实例化A类的对象,然后在实例化B类的时候还要将这个A类的对象手动的传参进去,是不是很麻烦。上面的A类与B类的依赖关系还不算复杂,如果还要依赖与其他的类,是不是更麻烦。了解了上面的弊端,我们就要想办法解决它,其实容器就是专门解决上面弊端的存在...
顾名思义,所谓的容器就是用来装东西的,这里提到的容器也一样,只不过是用来装类的实例化对象的。如果我们需要使用一个类的时候,直接在这个容器中取出来就好了,这样我们如果在多处需要使用某个类的时候,就不需要在N个地方进行实例化等操作了。最关键的是,如果我们的类存在依赖关系,我们不需要通过上面手动的方式进行注入等操作,容器可以帮助我们分析出所依赖的类,自动完成l注入等操作,听起来是不是很酷...下面,直接上代码,我们一点一点的来实现一个容器。

    //根据我们的上面的分析,容器至少需要俩个操作,分别是将类绑定到容器中以及将类从容器中取出的操作
    class Container
    {
        //容器类列表
        public static $generator_list = [];

        // 绑定类到容器中 
        public static function bind($class_name, $generator)
        {
            if (is_callable($generator)) {
                self::$generator_list[$class_name] = $generator;
            } else {
                throw new Exception('对象生成器不是可以调用的类型');
            }
        }

        // 生成类对象
        public static function make($class_name, $params = [])
        {
            if (! isset(self::$generator_list[$class_name])) {
                throw new Exception($class_name.'类没有被绑定注册');
            }
            return call_user_func_array(self::$generator_list[$class_name], $params);
        }
}复制代码

上面的十几行代码我们就实现了一个最基本的容器类了,看起来是不是很简单。但是我一开始看到这段代码的时候觉得并不简单,下面我们一一分析一下。

    我们先看一下bind()函数,该函数对应上面说到的绑定操作,就是将一个类放到$generator_list中,仔细看一下,你会发现,该函数并不是把一个类或者一个对象直接传递进去,而是传入了两个参数,一个是参数的名字,一个是生成器。
    生成器说白了就是一个函数,这个函数是用来负责实例化需要绑定的类的。
    说到这里,有同学可能有点疑惑,为什么要这样,为什么不直接传一个对象进去那?
    原因是类的实例化的过程是需要传递参数的,传递一个生成器进去,我们在实例化这个类的时候就可以修改参数了。
    下面就是一个绑定示例,大家可以看一下。
        Container::bind("A",function($param){
           return A($param);
        })
        self::$generator_list["A"] = function($param){
           return A($param);
        };
    这样,我们的绑定操作基本就说完了,下面看make()函数。之前也已经提到过了,make()函数就是将所需要的对象从这个容器中取出来。该函数也需要传递两个参数进去,一个是class_name也就是需要取出的类的名称,一个是params,也就是实例化对象的时候需要传递的参数。
    下面的一行代码是整个函数的关键所在:
    call_user_func_array(self::$generator_list[$class_name], $params);
    self::$generator_list[$class_name]对应的是类的生成器,$params对应的是类实例化所需要的参数,
    call_user_func_array()该函数是PHP的内置函数,通过该函数我们可以执行self::$generator_list[$class_name]对应的是类的生成器函数,这样我们也就是完成了所需类的实例化。(ps:对call_user_func_array()函数不清楚的同学可以先去看一下手册)复制代码

说了这么多,我写个测试代码看一下:

//将类A的生成器函数(匿名函数/闭包)绑定到容器中
Container::bind('A', function($name='') {
    return new A($title);
});
//在容器类中获取类A的对象
$Obj = Container::make('A', ['aaa']);
//打印出得到的这个对象
var_dump($Obj);
//打印结果如下:
object(A)#2 (1) {
  ["name"]=>
  string(4) "aaa"
}
//我们在打印出self::$generator_list中的数据看一下:
array(1) {
  ["A"]=>
  object(Closure)#1 (1) {
    ["parameter"]=>
    array(1) {
      ["$name"]=>
      string(10) ""
    }
  }
}复制代码

怎么样,看到上面打印出来的节后是不是就清楚一些了那...上面我们分析了一下容器类的具体的执行方式,上面的代码比较的简单,也灭有涉及到类相互依赖的问题,相比大家肯定想看一下类相互依赖的时候,容器类是怎么为我们解决依赖的,我们下面就写一个例子再分析一下,其实容器的代码我们基本不需要在动了。

//最开始的我们就举了一个B类依赖于A类的例子,现在我们继续使用这个例子来说明一下
//绑定A类到容器中
Container::bind('A', function($name='') {
    return new A($title);
});
//绑定B类到容器中
Container::bind('B', function($module,$params=[]) {
    return new B(Container::make($module,$params));
});
//上面B类的绑定方式大家可能觉得有点怪,这是因为B类依赖于A类,所以我们在B类的生成器对象中(匿名函数)中需要得到A类的实例传参给B,
//怎么获取A类的实例那,简单,因为A类也存在于容器中,所以我们直接调用make()函数就可以获取A类的实例对象了,
//但是在实例化A类的时候,构造函数可能需要参数,为了能够得到这些参数,我们就需要在B类的生成器对象中将这些参数传递进来。
//下面我们调用一下B类
$Obj= Container::make('B', ['A', ['aaa']]);
//上面我们就获取到了B类的实例化的对象了,是不是很简单,有兴趣的同学可以将上面的结果打印出来看一下。
//我们再分析一下上面的步骤,想要获取B类的实例化对象,直接通过make()进行获取,
//因为B依赖于A,所以需要传递A到生成器函数中,但是A有需要其他的参数,所以我们还需要继续传递其他参数进去,所以参数就是一个二维数组
//上面对参数有疑问的同学可以按照上面的流程分析一遍,就清楚了复制代码

上面我们通过容器的方式获取到了一个对其他类有依赖的类的实例对象,只不过我们是通过传参的方式完成依赖注入的。有的同学可能还觉得这样并不高级,因为还是需要我们再绑定类的时候(也就是bind()操作的时候),需要分析某个类所依赖的类的情况啊,当依赖变得很复杂的时候,开发维护起来还是很麻烦,这个问题怎么解决那???

4:如何替代手工分析类的依赖问题

那如何解决上面这个问题那???这就需要用到反射这个概念了,想必大家都听过这个概念,但其实在开发中使用到的概率并不高,主要是在框架开发中会用到这个功能。使用反射解决上面问题的原理就是,在实例化对象之前,先通过反射获取其构造函数所需的参数,分析出其所依赖的类,然后在容器中获取其所依赖的类,其实就是一层一层的找需要什么,需要什么就在容器中找什么,找到了就作为参数传递过去,这样就实现了自动注入解决了依赖的问题,是不是听起来很简单,但是这个反射的代码写起来需要考虑的地方还是很多的,反射也是现在框架开发中最常用的技术,也是核心技术之一。虽然反射听起来很厉害,但是在业务开发中并不推荐使用,因为对性能的影响还是很大的。但是为了实现自动注入,又不得不使用反射,这就是比较纠结的地方...关于反射部分的代码,我就不贴出来,水平有限,也不熟悉,写的实在不咋地,见谅!

关注下面的标签,发现更多相似文章
评论