背景
在很多编程语言(例如java)开发中,程序员在某个类中需要依赖其它类的方法,则通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理,一旦有修改,牵扯的类会很多。
最早在java的spring提出了依赖注入的思想,即依赖类不由程序员实例化,而是通过spring容器帮我们new指定实例并且将实例注入到需要该对象的类中。目前许多主流PHP框架也使用了依赖注入容器,如ThinkPHP、Laravel等。
一、概念
1、容器:字面上理解就是装东西的东西。常见的变量、对象属性等都可以算是容器。一个容器能够装什么,全部取决于你对该容器的定义。当然,现在我们讨论的是这样一种容器,它存放的不是文本、数值,而是对象、对象的描述(类、接口)或者是提供对象的回调(闭包),通过这种容器,我们得以实现许多高级的功能,其中最常提到的,就是 “解耦”、“依赖注入”。
2、IoC - Inversion of Control 控制反转
控制反转是从容器的角度在描述,即:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。
3、DI - Dependency Injection 依赖注入
依赖注入是从应用程序的角度在描述,可以把依赖注入,即:应用程序依赖容器创建并注入它所需要的外部资源。
备注:依赖注入和控制反转说的是同一个东西,是一种设计模式,这种设计模式用来减少程序间的耦合,从某个方面讲,就是它们描述的角度不同。
二、依赖注入的原理
一般情况下,当存在类与类之间的依赖关系的时候,我们都是通过直接实例化的方式进行调用。一旦出现多层依赖,这种方式的耦合程度就很高,在需要修改其中一个类的时候,会牵扯很多依赖它的类的修改,因此对代码的改动会比较大。
下面简单举一个A->B->C三层依赖的关系解释怎么运用依赖注入来解耦,提高开发效率。
![f3944f9acbb39ff9fc12b981f4496623.png](https://i-blog.csdnimg.cn/blog_migrate/1d31db1f8504fd26e18ee80fd1e1769b.jpeg)
而依赖注入方式如下:
![c3bd221dffd05b1ac17085258e71a547.png](https://i-blog.csdnimg.cn/blog_migrate/9991293121f5cf890067e8714d2103b8.jpeg)
解析:
常规写法里面,一旦C类需要作出改变,或者B类的调用需要改变成D类的时候,还需要考虑到依赖自己的B类,即还需要对B类作出修改。
依赖注入的思想就是即用即实例,反转类与类之间的控制关系,实现由调用类A类控制后续的依赖关系,这样可以让B类随意的更改所需依赖和实例化的类(C类或D类),达到解耦的目的。
![93d90341306ffc5fc4a1ec17ad85f3da.png](https://i-blog.csdnimg.cn/blog_migrate/86171cb6a623c897b9bec61065d57614.jpeg)
三、常用的依赖注入方式:
1、构造方法注入;2、set属性注入;3、静态工厂方法注入;
上述的例子使用的就是构造方法注入的方式,将对象作为参数传递到构造方法中;同样的set属性注入也是相类似的方法,不同的仅仅是在set一个类的成员的属性时传递这个对象参数,在此就不一一举例了。
除此之外,还有静态工厂方法注入的方式,这种方法与静态工厂方法类似。
我们知道静态工厂方法就是通过一个类来管理需要实例化的多个相似的类,该类会定义一个方法用于获取需要实例化的对象,而具体要实例化哪个对象就依赖于传递进来的对象名参数了。
对于静态工厂方式的注入,与一般的静态工厂方法不同之处在于这个传进来的参数是一个已经实例化过的对象。
<?phpclass IoC{ protected static $registry = []; public static function bind($name, Callable $resolver) //传入类名和类对象实例 { static::$registry[$name] = $resolver; } public static function make($name) //静态工厂方法 { if (isset(static::$registry[$name])) { $resolver = static::$registry[$name]; return $resolver(); //实例化 } throw new Exception('Alias does not exist in the IoC registry.'); }}
总而言之,三种方式传递的都是实例化对象,只是不同之处在于传递的位置分别为构造方法、set属性、静态工厂方法而已。
四、依赖注入容器(Ioc容器)
大多数时侯,在使用依赖注入方式解耦组件时,并不需要用到容器。
当一段程序需要实例化的类太多或者依赖太多的时候,重复依赖注入的代码是比较繁琐的事情,例如以下情况:
![e20ee84af559084fcd141c9750e80d1c.png](https://i-blog.csdnimg.cn/blog_migrate/650e46c0202b515f0d3b134af578b826.jpeg)
当产生以上关系的时候,依赖注入的代码会比较混乱,而且存在重复,更有可能在调用一个一般方法时new一个不需要的类,产生冗余。
此时需要使用容器,使用依赖注入容器后的思路是应用程序需要到A类,就从容器内取得A类。具体是容器创建C类,再创建B类并把C注入,再创建A类,并把B类注入,应用程序调用A类方法, A类调用B类方法,接着做些其它工作.总之容器负责实例化,注入依赖,处理依赖关系等工作。
![7a08295bb24788d4fcc88c94d18a0b09.png](https://i-blog.csdnimg.cn/blog_migrate/77683b24b5ffe5c7d88826e1b201989e.jpeg)
![1e9fb95f28d099100eeca801c6ed5384.png](https://i-blog.csdnimg.cn/blog_migrate/7f9f9ae3e7740a9ea33a3d1a6789c056.jpeg)
对于实际开发中复杂多变的代码环境,我们并不能完全知道现在的类在未来会扩展成什么情况,因此我们需要在有新的依赖类加入的时候,通过容器去实现实例化该类的方法。因此,在实例化未知类的时候,最能探索一个类的内部结构和实例化的方法就是利用反射,由此可知,反射是容器管理各个依赖类的核心。我们可以通过实例来了解容器的内部实现:
三个存在依赖关系的类:文件testClass.php
<?php //依赖关系:Company->Department->Groupclass Group{ public function doSomething() { echo __CLASS__.":".'hello', '|'; }}class Department{ private $group; public function __construct(Group $group) { $this->group = $group; } public function doSomething() { $this->group->doSomething(); echo __CLASS__.":".'hello', '|'; }}class Company{ private $department; public function __construct(Department $department) { $this->department = $department; } public function doSomething() { $this->department->doSomething(); echo __CLASS__.":".'hello', '|'; }}
Ioc容器的内部实现:
<?phpclass Container{ private $s = array(); public function __set($k, $c) { $this->s[$k] = $c; } public function __get($k) { return $this->build($this->s[$k]); } /** * 自动绑定(Autowiring)自动解析(Automatic Resolution) * * @param string $className * @return object * @throws Exception */ public function build($className) { // 如果是匿名函数(Anonymous functions),也叫闭包函数(closures) if ($className instanceof Closure) { // 执行闭包函数,并将结果 return $className($this); } /*通过反射获取类的内部结构,实例化类*/ $reflector = new ReflectionClass($className); // 检查类是否可实例化, 排除抽象类abstract和对象接口interface if (!$reflector->isInstantiable()) { throw new Exception("Can't instantiate this."); } /** @var ReflectionMethod $constructor 获取类的构造函数 */ $constructor = $reflector->getConstructor(); // 若无构造函数,直接实例化并返回 if (is_null($constructor)) { return new $className; } // 取构造函数参数,通过 ReflectionParameter 数组返回参数列表 $parameters = $constructor->getParameters(); // 递归解析构造函数的参数 $dependencies = $this->getDependencies($parameters); // 创建一个类的新实例,给出的参数将传递到类的构造函数。 return $reflector->newInstanceArgs($dependencies); } /** * @param array $parameters * @return array * @throws Exception */ public function getDependencies($parameters) { $dependencies = []; /** @var ReflectionParameter $parameter */ foreach ($parameters as $parameter) { /** @var ReflectionClass $dependency */ $dependency = $parameter->getClass(); if (is_null($dependency)) { // 是变量,有默认值则设置默认值 $dependencies[] = $this->resolveNonClass($parameter); } else { // 是一个类,递归解析 $dependencies[] = $this->build($dependency->name); } } return $dependencies; } /** * @param ReflectionParameter $parameter * @return mixed * @throws Exception */ public function resolveNonClass($parameter) { // 有默认值则返回默认值 if ($parameter->isDefaultValueAvailable()) { return $parameter->getDefaultValue(); } throw new Exception('I have no idea what to do here.'); }}require_once "./testclass.php"; //开始测试,先测试已知依赖关系的情况$c = new Container();$c->department = 'Department';$c->company = function ($c) { return new Company($c->department);};// 从容器中取得company$company = $c->company;$company->doSomething(); //输出: Group:hello|Department:hello|Company:hello|// 测试未知依赖关系,直接使用的方法$di = new Container();$di->company = 'Company';$company = $di->company;$company->doSomething();//输出: Group:hello|Department:hello|Company:hello|
我们可以通过一张图解释Ioc容器的内部逻辑:
![3d18b964957569fda0dfbf7bca6203d8.png](https://i-blog.csdnimg.cn/blog_migrate/ce1af5e58b4948336a2070bdb5a7fca6.jpeg)
五、总结
IOC的基本概念是:不创建对象,但是描述创建它们的方式。在代码中不直接与对象和服务连接,但在配置文件中描述哪一个组件需要哪一项服务。