谈谈php里的IOC控制反转,DI依赖注入_ccc82284的博客-程序员宅基地

技术标签: php  

理论

发现问题

在深入细节之前,需要确保我们理解"IOC控制反转"和"DI依赖注入"是什么,能够解决什么问题,这些在维基百科中有非常清晰的说明。

  • 控制反转(Inversion of Control,缩写为IoC):是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。
  • 依赖注入(Dependency Injection,简称DI):DI是IOC的一种实现,表现为:在类A的实例创建过程中即创建了依赖的B对象,通过类型名称来判断将不同的对象注入到不同的属性中。
  • 依赖查找(Dependency Lookup,简称DL):DL是IOC的另外一种实现,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象

依赖注入与依赖查找是控制反转的2种实现方式,后者很少见,我们主要研究依赖注入。

如果此前没有接触过这些概念,可能还是过于抽象不容易理解,但是下面这个场景你应该是见过的:

因为大多数应用程序都是由两个或是更多的类通过彼此的合作来实现业务逻辑,这使得每个对象都需要获取与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合并且难以维护和调试。

也就是说:"Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式的new一个B的对象",这就导致如果A想将B替换为一个更优的实现版本B+时,需要修改代码显式的new一个B+对象。

解决这个问题的传统做法一般是为B和B+提取一个InterfaceOfB接口,然后让class A只依赖InterfaceOfB,最终由A类的调用方决定传入B还是B+对象,修改调用方代码和修改类A代码对我们来说并没有本质的改变,那是否有更好的方式呢?

解决思路

终于,懒惰的程序员对这种代码开发方式感到厌烦:因为我们在代码里控制了B类对象的生成,从而导致代码耦合,即便A类依赖InterfaceOfB,还是要在程序某处写死new B()或者new B+()这样的代码,怎么破解?

答案是:将B类对象的生成交给一个独立的对象生成器来负责,那么A类只需要依赖这个对象生成器,而至于到底是生成B还是B+对象,则是对象生成器内部的行为,这样就将A和B解耦开了,这就是所谓的"控制反转",即将控制权交给了对象生成器。

这么简单的将问题抛给对象生成器可不行,因为对象生成器还要面临new B还是new B+的硬编码问题,因此必须赋予对象生成器一个超能力:

  • 在对象生成器的配置文件中进行这样的描述:{"InterfaceOfB" : "Class B+"},表示InterfaceOfB接口应该实例化B+对象。
  • A类构造函数有一个InterfaceOfB的入参,例如:function __construct(InterfaceOfB obj)。
  • 调用对象生成器(DI)获取A类对象,DI->get("class A")。对象生成器会利用反射分析class A的构造函数,发现InterfaceOfB参数后根据此前配置文件描述,new B+()对象传入到A的构造函数,从而生成A对象。

总结上述流程就是:对象生成器通过反射机制分析A类的构造函数依赖,并根据配置中的关系生成依赖的对象实例传入给构造函数,最终完成A类对象的创建。

上面的过程就是"依赖注入"主要实现方式了,对象生成器我们通常成为"DI Container",也就是"依赖注入容器"。

需要注意的是:B或者B+的构造函数可以会依赖InterfaceOfC,因此整个依赖关系的分析是递归的。

实践

上面在谈'DI依赖注入"的时候,我们非常清楚的了解到 DI会根据构造函数进行依赖分析,但是很容易忽视{"InterfaceOfB" : "Class B+"}这个信息的来源。如果DI不知道这个信息,那么在分析构造函数时是不可能知道接口InterfaceOfB应该对应什么对象的,这个信息在DI实现中一般是通过set方法主动设置到DI容器的依赖关系中的,当然这个信息的存储介质可以是配置文件或者硬编码传入。

下面拿PHP的Yii2.0框架为例,看看它实现DI时的核心思路是什么,不会讲的太细,但上面提到的思路和概念都会有所体现。

set设置类定义

public function set($class, $definition = [], array $params = [])
{
    $this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
    $this->_params[$class] = $params;
    unset($this->_singletons[$class]);
    return $this;
}

这就是上面提到{"InterfaceOfB" : "Class B+"}的设置接口,比如这样用:

$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');

意思就是如果遇到依赖MailInterface的,那么构造一个Mailer对象给它,params是用于传给Mailer::__construct的构造参数,之前提过依赖分析是递归的,Mailter对象的构造也是DI负责的(不是简单的new出来),一旦你传了构造参数给Mailer,那么DI就不用反射分析Mailter的依赖了,直接传入params既可new一个Mailer出来。

get生成类对象

public function get($class, $params = [], $config = [])
    {
        if (isset($this->_singletons[$class])) {
            // singleton
            // 此前已经get过并且设置为单例,那么返回单例对象既可
            return $this->_singletons[$class];
        } elseif (!isset($this->_definitions[$class])) {
            // 非单例需要生成新对象,但是此前没有set过类定义,
            // 因此只能直接反射分析构造函数的依赖
            return $this->build($class, $params, $config);
        }

        // 此前设置过的类定义,对类进行了更具体的定义,帮助我们更快的构造出对象
        $definition = $this->_definitions[$class];

        // 类定义可以是一个函数,用于直接为DI生成对象
        if (is_callable($definition, true)) {
            // 将set设置的构造参数和本次传入的构造参数merge到一起
            // 然后分析这些传入的构造参数是否为实参(比如:int,string),这是因为yii允许
            // params是Instance对象,它代表了另外一个类定义(它内部指向了DI容器中某个definition)
            // 为了这种构造参数能够传入到当前的构造函数,需要递归调用di->get将其创建为实参。
            $params = $this->resolveDependencies($this->mergeParams($class, $params));
            // 这个就是函数式的分配对象,前提是构造参数需要确保都是实参
            $object = call_user_func($definition, $this, $params, $config);
        } elseif (is_array($definition)) { // 普通的类定义
            $concrete = $definition['class'];
            unset($definition['class']);

            // 把set设置的config和这次传入的config合并一下
            $config = array_merge($definition, $config);
            // 把set设置的params构造参数和这次传入的构造参数合并一下
            $params = $this->mergeParams($class, $params);

            // 这里: $class代表的就是MailInterface,而$concrete代表的是Mailer
            if ($concrete === $class) {
                // 这里是递归出口,生成目标class对象既可,没有什么可研究的
                $object = $this->build($class, $params, $config);
            } else {
                // 显然,这里要构造MailInterface是等同于去构造Mailer对象
                $object = $this->get($concrete, $params, $config);
            }
        } elseif (is_object($definition)) {
            return $this->_singletons[$class] = $definition;
        } else {
            throw new InvalidConfigException("Unexpected object definition type: " . gettype($definition));
        }

        if (array_key_exists($class, $this->_singletons)) {
            // singleton
            $this->_singletons[$class] = $object;
        }

        return $object;
    }

实现思路在此前的分析里都说的很明白了,并不是很难理解。这个函数通过class指定要分配的类,params指定了构造参数,和之前的set原理一样:如果构造参数齐全是不需要分析依赖的。(最后的config是要注入到对象的额外属性,属于yii2特性,不是重点)。

至于build构造对象时,又需要做什么呢?就是基于反射机制获取构造函数依赖了哪些类,然后如果params传入了构造参数那么直接使用params参数,如果没有指定则需要递归的DI->get()去生成实参,最终通过构造函数生成对象。

 protected function build($class, $params, $config)
    {
        /* @var $reflection ReflectionClass */
        // 利用反射,分析类构造函数的参数,
        // 其中,返回值reflection是class的反射对象,
        // dependencies就是构造函数的所有参数了,有几种情况:
        // 1,参数有默认值,直接用
        // 2, 没有默认值,并且不是int这种非class,那么返回Instance指向对应的class,等待下面的递归get
        list ($reflection, $dependencies) = $this->getDependencies($class);

        // 传入的构造函数参数优先级最高,直接覆盖前面反射分析的构造参数
        foreach ($params as $index => $param) {
            $dependencies[$index] = $param;
        }

        // 完整的检查一次参数,如果依赖是指向class的Instance,那么递归DI->get获取实例
        // 如果是指定int,string这种的Instance,那么说明调用者并没有params传入值,构造函数默认参数也没有值,
        // 必须抛异常
        // 如果不是Instance,说明是params用户传入的实参可以直接用
        $dependencies = $this->resolveDependencies($dependencies, $reflection);
        if (empty($config)) {
            return $reflection->newInstanceArgs($dependencies);
        }
        // 最后通过反射对象,传入所有构造实参,完成对象创建
        if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
            // set $config as the last parameter (existing one will be overwritten)
            $dependencies[count($dependencies) - 1] = $config;
            return $reflection->newInstanceArgs($dependencies);
        } else {
            $object = $reflection->newInstanceArgs($dependencies);
            foreach ($config as $name => $value) {
                $object->$name = $value;
            }
            return $object;
        }
    }

如果你感兴趣可以看看getDependencies和resolveDependencies实现,前者缓存了每个类的反射信息(反射很耗费性能),后者体现了Instance的用法:代表尚未实例化的class类对象,需要DI->get获取。

  protected function getDependencies($class)
    {
        if (isset($this->_reflections[$class])) {
            return [$this->_reflections[$class], $this->_dependencies[$class]];
        }

        $dependencies = [];
        $reflection = new ReflectionClass($class);

        $constructor = $reflection->getConstructor();
        if ($constructor !== null) {
            foreach ($constructor->getParameters() as $param) {
                if ($param->isDefaultValueAvailable()) {
                    $dependencies[] = $param->getDefaultValue();
                } else {
                    $c = $param->getClass();
                    $dependencies[] = Instance::of($c === null   null : $c->getName());
                }
            }
        }

        $this->_reflections[$class] = $reflection;
        $this->_dependencies[$class] = $dependencies;

        return [$reflection, $dependencies];
    }
   protected function resolveDependencies($dependencies, $reflection = null)
    {
        foreach ($dependencies as $index => $dependency) {
            if ($dependency instanceof Instance) {
                if ($dependency->id !== null) {
                    $dependencies[$index] = $this->get($dependency->id);
                } elseif ($reflection !== null) {
                    $name = $reflection->getConstructor()->getParameters()[$index]->getName();
                    $class = $reflection->getName();
                    throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
                }
            }
        }
        return $dependencies;
    }

最后

最后还想简单说一下yii2的ServiceLoader,它基于DI Container封装了一层,将组件component单例维护在ServiceLoader内,而component的生成则通过DI Container实现。

不过有意思的是,ServiceLoader这样的实现并没能充分的使用DI Container的构造依赖注入能力,仅仅是传入component的class完成对象创建,最后注入了几个config指定的属性而已,并没有控制params的能力,这个可以看ServiceLoader中的set和get方法,然而这个设计基本要求了component的构造函数参数都应该能独立构造而不需要外部干预(干预是指DI->set进行类定义)。

除去ServiceLoader不谈,整个yii2.0框架也没找到可以通过配置文件自动化调用DI->set进行类定义的能力,硬编码属于走倒退的路,这基本上导致yii2.0对DI的应用能力停留在ServiceLoader层面,在递归解析依赖时也基本只能走无构造参数或者默认参数构造的路子。

正是在这种背景下,yii2.0的"依赖注入"也基本蜕化为ServiceLoader的get嵌套get,也就是类似"依赖查找"概念:在配置中分别写好A和B的component配置,并且配置A compoenent依赖B component,然后通过ServiceLoader得到A component,A类内部从配置中取出依赖的component(也就是B),最后通过ServiceLoader得到B component。

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/ccc82284/article/details/54142442

智能推荐

《HRNet-OCR:Object-Contextual Representations for Semantic Segmentation》论文笔记_m_buddy的博客-程序员宅基地

参考代码:HRNet-OCR1. 概述导读:这篇文章研究了语义分割中上下文信息的提取与表达,其方法设计的灵感来自于:分割中一个像素的label是其对应目标(object)的类别,那么基于此文章便是通过不同object区域的表达去关联并表达一个像素(特征图上)的信息,从而建立更加具有区分性质的上下文信息。对此完成该目的需要3个步骤:1)通过在GT的监督训练下得到图片的分割结果;2)在分割结果的基础上按照分割的类别划分区域(对应文章的object),使用区域中像素去计算区域的表达(OCR,Object

文件管理服务器win7,Win7如何取得文件管理所有权_shashashalalala的博客-程序员宅基地

电脑Win7如何取得文件管理所有权?从VISTA开始,微软对操作系统的安全性有了明显的提高,这样使得以前我们在XP下都可以打开或删除的文件(夹),无法在WIN7下进行操作。就算是在Administrator超级管理员帐户下也是无法操作的,提示“你需要来自TrustedInstaller的权限才能对此文件夹进行更改”,或者提示位置不可用,拒绝访问。特别是清理系统垃圾,不管采用什么工具,都无法清理掉被...

华为p30怎么删掉云相册的照片_原来华为这样清理内存,能清出几个G的内存!这下手机终于流畅了..._无边落木james的博客-程序员宅基地

阅读本文前,请您先点击上面的“蓝色字体”,再点击“关注”,这样您就可以继续免费收到最新文章了。每天都有分享。完全是免费订阅,请放心关注。声明:图文来源于网络,版权归原作者所有,如有侵权请联系我们删除! 手机一旦用久了,我们留在手机里的东西就会越来越多了,长期下来,占据的内存...

实参与形参不兼容怎么改_家居防护篇:“娇气”的实木家具如何度过梅雨季节?..._weixin_39644750的博客-程序员宅基地

梅雨季节即将到来,空气中弥漫着潮湿的气味,家具很容易出现返潮、发霉的情况。喜欢实木家具的朋友们在这个季节格外头疼。小北趁着梅雨季节还未来临之前,推荐一些比较实用的防潮小技巧,让“娇气”的实木可以度过梅雨季节。一、实木家具比较容易受潮气的侵袭,木材遇湿容易膨胀发霉,保持家具的通风透气成为至关重要的一点,我们该怎么做呢?家具保养的专家建议:雨季应将实木家具靠墙部分距离墙面1公分左右摆放,以免在潮湿季节...

linux 基础_:l|xhjlnbb_冬海潮的博客-程序员宅基地

linux 基础linux 系统的文件1. 文件与文件夹2.Linux系统文件目录结构/Bin:/dev:/boot :/etc :/etc/xinetd.d/ :/home :/lib:/media:/mnt:/opt:/root:/sbin :/srv:/tmp:Linux 的基本指令1. 指令与选项二、基础指令(重点)新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链...

放弃保研跨考计算机,为何总有不少学生放弃保研去考研,原来是有不得已的“苦衷”..._邢老板的博客-程序员宅基地

九月是推免的忙碌季,各个大学都会组织保研资格综合选拔、推免生复试。小西之前也发了几篇关于保研的文章,部分朋友留言"孩子推免已待录取,正在进行公示"。在和大家交流的过程中,就有人聊到放弃保研继续考研的同学。小西就来聊聊常见的几种放弃保研后去考研的情形:1.学校违规设定校内、校外推免限制,被迫考研。2016年研究生推免政策改革后,原则上只要是具有推免资格的高校,拿到保研资格后且意向高校同意待录取,任意...

随便推点

plex插件显示无服务器,deepin 15.11 安装plex和插件_whph的博客-程序员宅基地

想把老笔记本安装个媒体中心软件,尝试下Plex media server。1. 到plex中文网下载相应的deb包我下载的包是 plexmediaserver_1.18.7.2457-77cb9455c_amd64.deb2.安装也很简单sudo dpkg -i plexmediaserver_1.18.7.2457-77cb9455c_amd64.deb3.验证,看plex服务是否启动$sudo...

魅蓝note6Android怎么升级,魅蓝Note6拆解:首款双摄青年良品做工如何?_谈国平的博客-程序员宅基地

【PConline 拆解】2016年魅蓝是整个手机市场一个无法忽视的焦点,在每月一场的演唱会中推出了共10款魅蓝产品,让人目不暇接。2017年魅蓝回归理性,更加专注,这一点在魅蓝Note6上很好地体现了出来。煤油们期待已久的高通处理器、更加精细打磨的6系铝合金机身、旗舰级全像素双核对焦人像虚化双摄以及炫酷的4-LED流水式天线一体闪光灯等,不仅代表着魅蓝对“青年良品”的新诠释,更是对完美手机的另一...

多言统计及R语言建模按组距为300编制频数表,计算频数,频率和累积频率表,并绘制直方图_r语言按组距为300编制频数表_DataLiu的博客-程序员宅基地

按组距为300编制频数表,计算频数,频率和累积频率表,并绘制直方图某厂对50个计件工人某月份工资进行登记,获得以下原始资料(单位:元)试按组距为300编制频数表,计算频数,频率和累积频率表,并绘制直方图。要求写出用R语言进行基本统计的程序(数据可以从mvexec4.xls中获得)。 获取数据的变量名和数据的长度如何求频数小写x是excel中研究数据的变量名 names(x) [1]...

qt+opencv+opencv_contrib+cmake_瞅瞅小龙的博客-程序员宅基地

由于opencv中的好多新库都在opencv3版本种,因此装了opencv3;此时cmake用3.4版本由于opencv3不支持低版本的qt,所以装了qt5.选择的版本分别为:qt :5.6.3opencv: 3.1.0opencv_contrib : 3.1.0   这一行的两个最好保持版本一致,否则可能出现安装一半奔溃cmake : 3.4.11,安装qt, 之后一定要

HTML基础知识_weixin_33895657的博客-程序员宅基地

HTML、CSS、JS的理解1.HTML是网页内容的载体,内容包括网页制作者想要让用户浏览的信息,包括文字、图片、视频等。2.CSS是表现,用来改变网页内容的外观的东西。3.JS是用来实现网页上的特效,以及用来实现交互。4.用HTML建立页面结构,用CSS指定样式,不要用HTML指定样式。注意1.浏览器会忽略HTML文档中的制表符、回车和大部分空格。2.虽然有些情况下语法错误浏览器还...

ML--文本数据处理_weixin_30492047的博客-程序员宅基地

ML–文本数据处理一直以来,自然语言处理(Natual Language Processing,NLP)作为人工智能的重要分支之一,其研究的内容是如何实现人与计算机之间用自然语言进行有效的通信。自然语言处理中的基础知识–如何对文本数据进行处理主要涉及的知识点有:文本数据的特征提取中文文本的分词办法用n-Gram模型优化文本数据...