对PHP框架中的容器的理解

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

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

    class A
    {
       public $name = "";
       public function __construct($name)
       {
          $this->name = $name;
          echo '我是被依赖的类...';
       }
 
    }
 
    class B
    {
      public function __construct()
      {
         echo '我依赖于类A实现业务逻辑...';
         require "A类的类文件";
         $Obj = new A("aaa");
         $Obj->func() //.....等等方法   
      }
    }

们没用几行代码就解决了刚才提到的问题,相信很多人一开始写代码的时候都会这么写的。可是这种写法存在一个很大的问题,就是当A类发生变动的时候,比如A类的构造函数需要传入多个参数了,这时我们需要手动的去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类,这样就不需要在B类中写引入或者实例化A类的代码了,因为我们传递过去的就是一个对象嘛,所以就降低了对A类的依赖。这种 类型 + 参数定义形参的方式,可以提供类型约束。上面的代码中,B类中约束了传递过来的参数必须是 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:如何替代手工分析类的依赖问题,使用反射

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

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值