一、为什么需要依赖注入

  首先我们先不管什么是依赖注入,先来分析一下没有使用依赖注入会有什么样的结果。假设我们有一个gmail邮件服务类GMail,然后有另一个类User,User类需要使用发邮件的功能,于是我们在User类中定义一个成员变量$mailServer,并且在声明这个变量的时候就给它赋值一个GMail类对象,或者在User构造函数中进行GMail类实例化与赋值。这样写程序会有什么问题呢?试想一下,每次当我们需要把User使用的邮件服务改为其他类型邮件服务的时候,我们需要频繁修改User类的成员变量$mailServer,这样是不好的。问题的根源就在于,我们不该把User类的成员变量$mailServer的实例化写死在User类内部,而应该在调用User类的时候可以动态决定赋值给$mailServer的对象类型,依赖注入就是来解决这个问题的。

二、依赖注入是什么

  所谓依赖注入,实质上就是当某个类对象需要使用另一个类实例的时候,不在类内部实例化另一个类,而将实例化的过程放在类外面实现,实例化完成后再赋值给类对象的某个属性。这样的话该类不需要知道赋值给它的属性的对象具体属于哪个类的,当需要改变这个属性的类型的时候,无需对这个类的代码进行任何改动,只需要在使用该类的地方修改实例化的代码即可。

  依赖注入的方式有两种:1.构造函数注入,将另一个类的对象作为参数传递给当前类的构造函数,在构造函数中给当前类属性赋值;2.属性注入,可以将该类某个属性设置为public属性,也可以编写这个属性的setter方法,这样就可以在类外面给这个属性赋值了。

三、依赖注入容器

  仔细思考一下,我们会发现,虽然依赖注入解决了可能需要频繁修改类内部代码的问题,但是却带来了另一个问题。每次我们需要用到某个类对象的时候,我们都需要把这个类依赖的类都实例化,所以我们需要重复写这些实例化的代码,而且当依赖的类又依赖于其他类的时候,我们还要找出所有依赖类依赖的其他类然后实例化,可想而知,这是一个繁琐低效而又麻烦且容易出错的过程。这个时候依赖注入容器应运而生,它就是来解决这个问题的。

  依赖注入容器可以帮我们实例化和配置对象及其所有依赖对象,它会递归分析类的依赖关系并实例化所有依赖,而不需要我们去为这个事情费神。

  在yii2.0中,yii\di\Container就是依赖注入容器,这里先简单说一下这个容器的使用。我们可以使用该类的set()方法来注册一个类的依赖,把依赖信息传递给它就可以了,如果希望这个类是单例的,则可以使用setSingleton()方法注册依赖。注册依赖之后,当你需要这个类的对象的时候,使用Yii::createObject(),把类的配置参数传递过去,yii\di\Container即会帮你解决这个类的所有依赖并创建一个对象返回。

四、yii依赖注入容器 - 依赖注册

  好了,下面开始分析一下yii的依赖注入容器的实现原理。首先来看一下Container的几个成员变量:

/**
* @var array 存储单例对象,数组的键是对象所属类的名称
*/
private $_singletons = []; /**
* @var array 存储依赖定义,数组的键是对象所属类的名称
*/
private $_definitions = []; /**
* @var array 存储构造函数参数,数组的键是对象所属类的名称
*/
private $_params = [];
/**
* @var array 存储类的反射类对象,数组的键是类名或接口名
*/
private $_reflections = []; /**
* @var array 存储类的依赖信息,数组的键是类名或接口名
*/
private $_dependencies = [];

其中前三个是用于依赖注册的时候存储一些类参数和依赖定义的,后两个则是用于存储依赖信息的,这样使用同一个类的时候不用每次都进行依赖解析,直接使用这两个变量缓存的依赖信息即可。

  接下来看看依赖注册的两个方法:

/**
* 在DI容器注册依赖(注册之后每次请求都将返回一个新的实例)
* @param string $class:类名、接口名或别名
* @param array $definition:类的依赖定义,可以是一个PHP回调函数,一个配置数组或者一个表示类名的字符串
* @param array $params:构造函数的参数列表,在调用DI容器的get()方法获取类实例的时候将被传递给类的构造函数
* @return \yii\di\Container
*/
public function set($class, $definition = [], array $params = [])
{
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);//保存类配置信息
$this->_params[$class] = $params;//保存构造函数参数列表
unset($this->_singletons[$class]);//若存在单例依赖信息则删除
return $this;
} /**
* 在DI容器注册依赖(注册之后每次请求都将返回同一个实例)
* @param string $class:类名、接口名或别名
* @param array $definition:类的依赖定义,可以是一个PHP回调函数,一个配置数组或者一个表示类名的字符串
* @param array $params:构造函数的参数列表,在调用DI容器的get()方法获取类实例的时候将被传递给类的构造函数
* @return \yii\di\Container
*/
public function setSingleton($class, $definition = [], array $params = [])
{
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
$this->_params[$class] = $params;
$this->_singletons[$class] = null;//赋值null表示尚未实例化
return $this;
}

这两个方法很简单,就是把依赖注册传入的参数信息保存下来,提供给实例化过程使用。这两个方法中都调用了normalizeDefinition()方法,这个方法只是用于规范依赖定义的,源码如下:

/**
* 规范依赖定义
* @param string $class:类名称
* @param array $definition:依赖定义
* @return type
* @throws InvalidConfigException
*/
protected function normalizeDefinition($class, $definition)
{
if (empty($definition)) {//若为空,将$class作为类名称
return ['class' => $class];
} elseif (is_string($definition)) {//若是字符串,默认其为类名称
return ['class' => $definition];
} elseif (is_callable($definition, true) || is_object($definition)) {//若是PHP回调函数或对象,直接作为依赖的定义
return $definition;
} elseif (is_array($definition)) {//若是数组则需要确保包含了类名称
if (!isset($definition['class'])) {
if (strpos($class, '\\') !== false) {
$definition['class'] = $class;
} else {
throw new InvalidConfigException("A class definition requires a \"class\" member.");
}
}
return $definition;
} else {
throw new InvalidConfigException("Unsupported definition type for \"$class\": " . gettype($definition));
}
}

五、yii依赖注入容器 - 对象实例化

  接下来就是重头戏了,yii依赖注入容器是怎么根据依赖注册的信息实现对象实例化的呢?我们一步一步来分析。在第三部分我们说到,当需要创建一个类对象的时候,我们调用的时候Yii::createObject()方法,这个方法里面调用的是Container的get()方法。为了使得讲解的思路更清晰,这里我们先来看一下Container的另外两个方法,getDependencies()和resolveDependencies(),它们分别用于解析类的依赖信息和解决类依赖,会在对象实例化的过程中被调用。

  下面先来看看getDependencies()方法:

/**
* 解析指定类的依赖信息(利用PHP的反射机制)
* @param string $class:类名、接口名或别名
* @return type
*/
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];
}

首先判断该类是已被解析过,如果是,直接返回缓存中该类的依赖信息,否则,利用PHP的反射机制对类的依赖进行分析,最后将分析所得依赖信息缓存,具体步骤已在代码中注明。其中Instance类实例用于表示一个指定名称的类的对象引用,也就是说getDependencies()方法调用之后,得到的$dependencies只是某个类的依赖信息,指明这个类依赖于哪些类,还没有将这些依赖的类实例化,这个工作是由resolveDependencies()方法来完成的。

  再来看看resolveDependencies()方法:

/**
* 解决依赖
* @param array $dependencies:依赖信息
* @param ReflectionClass $reflection:放射类对象
* @return type
* @throws InvalidConfigException
*/
protected function resolveDependencies($dependencies, $reflection = null)
{
foreach ($dependencies as $index => $dependency) {//遍历依赖信息数组,把所有的对象引用都替换为对应类的实例对象
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {//组件id不为null,以id为类名实例化对象
$dependencies[$index] = $this->get($dependency->id);
} elseif ($reflection !== null) {//若id为null但$reflection不为null,通过$reflection获取构造函数类型,报错。。
$name = $reflection->getConstructor()->getParameters()[$index]->getName();
$class = $reflection->getName();
throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
}
}
}
return $dependencies;
}

这个方法就是遍历getDependencies()方法得到的关于某个类的依赖信息数组,对每个依赖的类调用Container的get()方法来获取对象实例化。前面说到get()函数实例化过程中会调用这个方法,而这里又调用了get()方法,所以已经可以知道,依赖解析的过程其实是一个递归解析的过程。

  再回头来看看get()方法:

/**
* DI容器返回一个请求类的实例
* @param string $class:类名
* @param array $params:构造函数参数值列表,按照构造函数中参数的顺序排列
* @param array $config:用于初始化对象属性的配置数组
* @return type
* @throws InvalidConfigException
*/
public function get($class, $params = [], $config = [])
{
if (isset($this->_singletons[$class])) {//若存在此类的单例则直接返回单例
return $this->_singletons[$class];
} elseif (!isset($this->_definitions[$class])) {//若该类未注册依赖,调用build()函数,使用PHP的反射机制获取该类的依赖信息,解决依赖,创建类对象返回
return $this->build($class, $params, $config);
} $definition = $this->_definitions[$class]; if (is_callable($definition, true)) {//若依赖定义是一个PHP函数则直接调用这个函数创建对象
$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']); $config = array_merge($definition, $config);
$params = $this->mergeParams($class, $params); if ($concrete === $class) {//递归解析终止
$object = $this->build($class, $params, $config);
} else {//递归进行依赖解析
$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)) {//若该类注册过单例依赖则实例化之
$this->_singletons[$class] = $object;
} return $object;
}

首先判断是否存在所需类的单例,若存在则直接返回单例,否则判断该类是否已注册依赖,若已注册依赖,则根据注册的依赖定义创建对象,具体每一步已在代码注释说明。其中mergeParams()方法只是用来合并用户指定的参数和依赖注册信息中的参数。若未注册依赖,则调用build()方法,这个方法又干了些什么呢?看源码:

/**
* 创建指定类的对象
* @param string $class:类名称
* @param array $params:构造函数参数列表
* @param array $config:初始化类对象属性的配置数组
* @return type
* @throws NotInstantiableException
*/
protected function build($class, $params, $config)
{
list ($reflection, $dependencies) = $this->getDependencies($class);//获取该类的依赖信息 foreach ($params as $index => $param) {//将构造函数参数列表加入该类的依赖信息中
$dependencies[$index] = $param;
} $dependencies = $this->resolveDependencies($dependencies, $reflection);//解决依赖,实例化所有依赖的对象
if (!$reflection->isInstantiable()) {//类不可实例化
throw new NotInstantiableException($reflection->name);
}
if (empty($config)) {//配置数组为空,使用依赖信息数组创建对象
return $reflection->newInstanceArgs($dependencies);
} if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
$dependencies[count($dependencies) - 1] = $config;//按照 Object 类的要求,构造函数的最后一个参数为 $config 数组
return $reflection->newInstanceArgs($dependencies);
} else {//先使用依赖信息创建对象,再使用$config配置初始化对象属性
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}

首先,由于没有类的依赖信息,调用getDependencies()方法分析得到依赖信息。然后调用resolveDependencies()方法解决依赖,实例化所有依赖类对象,最后就是创建对象了。

yii2之依赖注入与依赖注入容器的更多相关文章

  1. yii依赖注入和依赖注入容器

    依赖注入和依赖注入容器¶ 为了降低代码耦合程度,提高项目的可维护性,Yii采用多许多当下最流行又相对成熟的设计模式,包括了依赖注入(Denpdency Injection, DI)和服务定位器(Ser ...

  2. 理解依赖注入,laravel IoC容器

    在看laravel文档的时候,有一个服务容器(IoC)的概念.它是这样介绍的:Laravel 服务容器是一个用于管理类依赖和执行依赖注入的强大工具.依赖注入听上去很花哨,其实质是通过构造函数或者某些情 ...

  3. Spring.NET依赖注入框架学习--实例化容器常用方法

    Spring.NET依赖注入框架学习---实例化容器常用方法 本篇学习实例化Spring.NET容器的俩种方式 1.通过XmlObjectFactory创建一个Spring.NET容器 IResour ...

  4. IoC模式(依赖、依赖倒置、依赖注入、控制反转)

    1.依赖 依赖就是有联系,有地方使用到它就是有依赖它,一个系统不可能完全避免依赖.如果你的一个类或者模块在项目中没有用到它,恭喜你,可以从项目中剔除它或者排除它了,因为没有一个地方会依赖它.下面看一个 ...

  5. spring依赖注入之构造函数注入,set方法注入

    <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.spr ...

  6. Spring 依赖注入中 Field 注入的有害性

    大致分为:Field 注入.构造注入.setter 注入 其中 Field 注入被认为有害的: 1. 违反了单一原则 当一个 class 中有多个依赖时,如果仅仅使用 Field 注入,则看不出有很多 ...

  7. PHP 依赖注入,依赖反转 (IOC-DI)

    https://my.oschina.net/u/3529405/blog/1821744 <?php /** * 依赖注入 IOC DI * 参考文章 * https://segmentfau ...

  8. 轻松学,浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI) 依赖注入和控制反转的理解,写的太好了。

    轻松学,浅析依赖倒置(DIP).控制反转(IOC)和依赖注入(DI) 2017年07月13日 22:04:39 frank909 阅读数:14269更多 所属专栏: Java 反射基础知识与实战   ...

  9. Spring学习笔记1—依赖注入(构造器注入、set注入和注解注入)

    什么是依赖注入 在以前的java开发中,某个类中需要依赖其它类的方法时,通常是new一个依赖类再调用类实例的方法,这种方法耦合度太高并且不容易测试,spring提出了依赖注入的思想,即依赖类不由程序员 ...

随机推荐

  1. Python2.7笔记——常用技术点汇总

    目录 · 概况 · 安装 · 基础 · 基础语法 · 数据类型 · 变量 · 常量 · 字符编码 · 字符串格式化 · list · tuple · dict · set · if语句 · for语句 ...

  2. 网络基础之IP地址与子网划分

    IP地址 Ipv4地址格式:点分十进制 IP地址的分类 A类 B类 C类: D类:组播 E类: 公共IP地址 私有IP地址 特殊地址 保留地址 子网掩码 什么是子网掩码 CIDR表示法 子网划分 为啥 ...

  3. Java实现Map集合二级联动

    Map集合可以保存键值映射关系,这非常适合本实例所需要的数据结构,所有省份信息可以保存为Map集合的键,而每个键可以保存对应的城市信息,本实例就是利用Map集合实现了省市级联选择框,当选择省份信息时, ...

  4. Windows的文件权限 研究笔记

    最近公司的一台设备中了病毒,杀了又出现,总是破坏机器上面运行的程序. 想着研究下文件权限,把文件设为只读,让病毒破坏不了即可. 于是开始了实验1: 首先建立一个txt文件,查看权限: 可以看到User ...

  5. <c:forEach>+<c:if>

    <c:forEach>:用来做循环<c:if>:相当于if语句用于判断执行,如果表达式的值为 true 则执行其主体内容. <c:forEach var="每个 ...

  6. Redis常见的应用场景解析

    Redis是一个key-value存储系统,现在在各种系统中的使用越来越多,大部分情况下是因为其高性能的特性,被当做缓存使用,这里介绍下Redis经常遇到的使用场景. Redis特性 一个产品的使用场 ...

  7. CSS3微信启动页天王星版

    今天被微信启动页刷屏了. 一直还以为启动页背景显示的月球的.今天才了解到这么有含义. 我也蹭一下微信的热度,做一个HTML+CSS版本的. 用CSS画地球太困难了,来个简单点的,天王星版. 主要使用到 ...

  8. 1.Bootstrap-简介

    1.介绍 Bootstrap 是一个用于快速开发 Web 应用程序和网站的前端框架.Bootstrap 是基于 HTML.CSS.JAVASCRIPT 的. 2.HTML 模板 一个使用了 Boots ...

  9. spring web.xml配置

    <!--推荐使用此种方式-->  <listener> <listener-class> org.springframework.web.context.Conte ...

  10. JPEG流封装AVI视频

    前言:前几天工作任务,要把JPEG流封装为AVI视频,就找了些AVI文件结构资料和示例代码研究了下,现将学习总结及最终完成的可用代码分享出来,由于本人也是现学现用,如有不恰当或错误之处,欢迎提出! 1 ...