谈谈php里的IOC控制反转,DI依赖注入(转)
转自:http://www.cnblogs.com/qq120848369/p/6129483.html
发现问题
在深入细节之前,需要确保我们理解"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设置类定义
1
2
3
4
5
6
7
|
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+"}的设置接口,比如这样用:
1
|
$container ->set( 'yii\mail\MailInterface' , 'yii\swiftmailer\Mailer' ); |
意思就是如果遇到依赖MailInterface的,那么构造一个Mailer对象给它,params是用于传给Mailer::__construct的构造参数,之前提过依赖分析是递归的,Mailter对象的构造也是DI负责的(不是简单的new出来),一旦你传了构造参数给Mailer,那么DI就不用反射分析Mailter的依赖了,直接传入params既可new一个Mailer出来。
get生成类对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
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()去生成实参,最终通过构造函数生成对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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获取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
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。
谈谈php里的IOC控制反转,DI依赖注入(转)的更多相关文章
- laravel服务容器(IOC控制反转,DI依赖注入),服务提供者,门脸模式
laravel的核心思想: 服务容器: 容器:就是装东西的,laravel就是一个个的对象 放入:叫绑定 拿出:解析 使用容器的目的:这里面讲到的是IOC控制反转,主要是靠第三方来处理具体依赖关系的解 ...
- Spring 04: IOC控制反转 + DI依赖注入
Spring中的IOC 一种思想,两种实现方式 IOC (Inversion of Control):控制反转,是一种概念和思想,指由Spring容器完成对象创建和依赖注入 核心业务:(a)对象的创建 ...
- Spring专题2: DI,IOC 控制反转和依赖注入
合集目录 Spring专题2: DI,IOC 控制反转和依赖注入 https://docs.spring.io/spring/docs/2.5.x/reference/aop.html https:/ ...
- Java Web实现IOC控制反转之依赖注入
控制反转(Inversion of Control,英文缩写为IoC)是一个重要的面向对象编程的法则来削减计算机程序的耦合问题,也是轻量级的Spring框架的核心. 控制反转一般分为两种类型,依赖注入 ...
- (转)Ioc控制反转和依赖注入
转载地址:https://zhuanlan.zhihu.com/p/95869440 控制反转控制反转(Inversion of Control,简称IoC),是面向对象编程中的一种设计思想,其作用是 ...
- 谈谈php里的IOC控制反转,DI依赖注入
理论 发现问题 在深入细节之前,需要确保我们理解"IOC控制反转"和"DI依赖注入"是什么,能够解决什么问题,这些在维基百科中有非常清晰的说明. 控制反转(In ...
- Spring的IOC控制反转和依赖注入-重点-spring核心之一
IoC:Inverse of Control(控制反转): 读作"反转控制",更好理解,不是什么技术,而是一种设计思想,好比于MVC.就是将原本在程序中手动创建对象的控制权,交由S ...
- spring IOC --- 控制反转(依赖注入)----简单的实例
IoC(Inversion of Control)控制反转,对象创建责任的反转,在spring中BeanFacotory是IoC容器的核心接口,负责实例化,定位,配置应用程序中的对象及建立这些对象间的 ...
- 搞定.NET MVC IOC控制反转,依赖注入
一直听说IOC,但是一直没接触过,只看例子好像很高达上的样子,今天抽了点时间实现了下,当然也是借助博客园里面很多前辈的文章来搞的!现在做个笔记,防止自己以后忘记! 1.首先创建MVC项目 2.然后新建 ...
随机推荐
- 常用Maven插件介绍(转载)
我们都知道Maven本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由maven- compiler-plugin完成的.进一步说,每个任务对应 ...
- 如何使用keil5将stm32的hal库编译成lib文件——F1版本
hal库中keil5中编译的速度是比较慢的,相同情况下,每次都要编译的时候,比标准库是要慢很多的,因此就hal库编译成lib文件是一种加快编译速度的方法,当然也有其自身的缺点.一.步骤1.使用cube ...
- Java 多线程实战
Java多线程 public class ThreadTest { public static void main(String[] args) throws InterruptedException ...
- 51nod1463 找朋友
[传送门] 写的时候一直没有想到离线解法,反而想到两个比较有趣的解法.一是分块,$f[i][j]$表示第$i$块块首元素到第$j$个元素之间满足条件的最大值(即对$B_l + B_r \in K$的$ ...
- [CodeForces 663E] - Binary Table(FWT)
题目 Codeforces 题目链接 分析 大佬博客,写的很好 本蒟蒻就不赘述了,就是一个看不出来的异或卷积 精髓在于 mask对sta的影响,显然操作后的结果为mask ^ sta AC code ...
- javascript 终极循环方法for... of ..推荐
js目前有很多的循环方法,如for, forEach, for .. in, for of 等等,而在ES6里面,我们又增加了一些数据结构,比如set,map,Symbol等. 那么我们该选取哪一 ...
- JS实现Base64编码、解码,即window.atob,window.btoa功能
window.atob(),window.btoa()方法可以对字符串精选base64编码和解码,但是有些环境比如nuxt的服务端环境没法使用window,所以需要自己实现一个base64的编码解码功 ...
- P4936 题解
\(\text{Update}\)(2019.10.05): 递推公式推法更详细: 通项公式更新详细版: 单位矩阵的推法更加详细. 特别鸣谢 @Smallbasic 苣佬,是他教会了我推递推公式和通项 ...
- git:GitLab代码回滚到特定版本
在当前branch上多次commit代码并且push后,发现不符合要求,需要回滚到特定的版本.步骤如下: 1.查找commitId (1)用命令行打开git项目路径,输入git log命令查看comm ...
- Shell的语法
Shell的语法: 变量:字符串.数字.环境和参数: 条件:shell中的布尔值: 程序控制:if.elif.for.while.until.case: 命令列表: 函数: Shell内置命令: 获取 ...