前言:
这是我第二个博文说到DI,IOC,上次说的是spring实现DI的一个方式。今天要说的是Yii2.0 实现的DI,实际上有很多相同之处。在实现解析依赖的时候,都采用了反射的思想。
(何为反射?如果把php运行时看做是一面镜子, 把php代码看做是一个物体。当你的程序在php运行时运行的时候, 你的程序想知道你现在运行的php代码的一些信息。 比如说我想动态调用一个类的方法,或者我想知道我的一个类到底有哪些方法,有哪些属性。 这就叫做反射。)
该博文参考 深入理解Yii2.0 http://www.digpage.com/di.html ,写得非常详细清楚,但是我感觉他写的有些枯燥,上来就是概念源码。所以我这里改变一下策略,简单说下概念 之后直接先说例子。
概念
· 依赖倒置原则(Dependence Inversion Principle, DIP)
· 控制反转(Inversion of Control, IoC)
· 依赖注入(Dependence Injection, DI)
先区分以上概念
DIP是一种软件设计的指导思想。
传统软件设计中,上层代码依赖于下层代码,当下层出现变动时, 上层代码也要相应变化,维护成本较高。
DIP的核心思想是上层定义接口,下层实现这个接口, 从而使得下层依赖于上层,降低耦合度,提高整个系统的弹性。
IoC也是一种指导思想。是一种实现DIP的方法。
IoC的核心是将类(上层)所依赖的单元(下层)的实例化过程交由第三方来实现。在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。
DI是IoC的一种设计模式(套路)。
按照DI的套路,可以实现IoC。DI 和 IoC是同一个概念的不同角度描述,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。
先看Yii官方例子
namespace app\models;
use yii\base\Object;
use yii\db\Connection;
// 定义接口
interface UserFinderInterface
{
function findUser();
}
// 定义类,实现接口
class UserFinder extends Object implements UserFinderInterface
{
public $db;
// 从构造函数看,这个类依赖于 Connection
public function __construct(Connection $db, $config = [])
{
$this->db = $db;
parent::__construct($config);
}
public function findUser()
{
}
}
class UserLister extends Object
{
public $finder;
// 从构造函数看,这个类依赖于 UserFinderInterface接口
public function __construct(UserFinderInterface $finder, $config = [])
{
$this->finder = $finder;
parent::__construct($config);
}
}
1.我们看到上面的代码,可以归纳他的依赖关系
UserLister 类依赖于接口 UserFinderInterface , 而接口有一个实现就是 UserFinder 类,但这类又依赖于 Connection。
画个图理解下:
2.然后我们迅速写一下他的实例化
$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);
上面这段代码就是逆着依赖关系来从最底层Connection开始实例化。这个顺序不能改变。
不使用DI的后果:
这个关系在上面的例子上看起来很简单,但单依赖关系达到10-20个,就会很麻烦了。要特别注意他们的依赖关系,顺序不能搞错了。实例化的代码也会很多。
团队开发的时候,很多模块是共用的,比如邮件投递服务。这个我之前的项目也遇到过,在修改账号密码,在注册,在后台管理等等都要调用到这个邮件投递的组件,或者说模块。对,我采用的是直接实例化,但如果这不是我自己的项目,是公司的大型项目,如果有10个模块调用到邮件投递服务,难道我要在代码不同地方new 10 次?OK , 假设大家都这么干了,但比如说我这个邮件投递模块是Gmail的,然后有一天Google说他不干了,Gmail下线,然后我们的程序员迅速把Gmail改了然后他把类名修正过来,不叫Gmail了,OK,他要在庞大的代码中找到10处实现了Gmail的代码,把Gmail给改过来。
于是乎,Yii2.0 和spring 都采用了DI的设计模式来解决以上的问题。spring怎么实现的,可以参考我之前的博客。接下来说Yii2.0是怎么实现的。
Yii 2.0 实现DI
还是先说上面的例子的Yii2.0 的解决方法。
use yii\di\Container;
// 创建一个DI容器
$container = new Container;
// 为Connection指定一个数组作为依赖,当需要Connection的实例时,
// 使用这个数组进行创建
$container->set('yii\db\Connection', [
'dsn' => '...',
]);
// 在需要使用接口 UserFinderInterface 时,采用UserFinder类实现
$container->set('app\models\UserFinderInterface', [
'class' => 'app\models\UserFinder',
]);
// 为UserLister定义一个别名
$container->set('userLister', 'app\models\UserLister');
// 获取这个UserList的实例
$lister = $container->get('userLister');
先不管这个Container类的源码是怎么实现的。我们就先从一个使用者的角度来理解。
先把set函数归为一起看,set()就是写入特定的数据结构,没有涉及到具体依赖关系的解析。所以,前后关系是不重要的,不需要考虑他们的依赖关系。
然后我们再思考,那怎么知道Container怎么知道它们之间的依赖关系呢。这就是回到了我为什么要在博文一开头介绍反射的思想了。通过反射我们就可以知道类以及他们构造函数中的依赖关系。(这里要用到ReflectionClass::getConstructor, 通过读取构造函数的参数可以知道依赖关系)
上面的代码只有一个get() 看起来好像根本没有实例化其他如 Connection 单元一样,但事实上,DI容器已经安排好了一切。 在获取 userLister 之前, Connection 和 UserFinder 都会被自动实例化。 其中, Connection 是根据依赖定义中的配置数组进行实例化的。
所以一切的精华,一切的秘密都在get(‘userLister’)。
想要了解这个实现的话,就要涉及到源码了。Yii 通过 yii\di\Container 类提供 DI 容器特性。它支持如下几种类型的依赖注入:
构造方法注入;
Setter 和属性注入;
PHP 回调注入.
Yii2.0 DI 源码
未完待续。