标签:
为了降低代码耦合程度,提高项目的可维护性,Yii采用多许多当下最流行又相对成熟的设计模式,包括了依赖注入(Denpdency Injection, DI)和服务定位器(Service Locator)两种模式。 关于依赖注入与服务定位器, Inversion of Control Containers and the Dependency Injection pattern 给出了很详细的讲解,这里结合Web应用和Yii具体实现进行探讨,以加深印象和理解。 这些设计模式对于提高自身的设计水平很有帮助,这也是我们学习Yii的一个重要出发点。
在了解Service Locator 和 Dependency Injection 之前,有必要先来了解一些高大上的概念。 别担心,你只需要有个大致了解就OK了,如果展开来说,这些东西可以单独写个研究报告:
是不是云里雾里的?没错,所谓“高大上”的玩意往往就是这样,看着很炫,很唬人。 卖护肤品的难道会跟你说其实皮肤表层是角质层,不具吸收功能么?这玩意又不考试,大致意会下就OK了。 万一哪天要在妹子面前要装一把范儿的时候,张口也能来这么几个“高大上”就行了。 但具体的内涵,我们还是要要通过下面的学习来加深理解,毕竟要把“高大上”的东西用好,发挥出作用来。
首先讲讲DI。在Web应用中,很常见的是使用各种第三方Web Service实现特定的功能,比如发送邮件、推送微博等。 假设要实现当访客在博客上发表评论后,向博文的作者发送Email的功能,通常代码会是这样:
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 |
// 为邮件服务定义抽象层
interface EmailSenderInterface
{
public function send(...);
}
// 定义Gmail邮件服务
class GmailSender implements EmailSenderInterface
{
...
// 实现发送邮件的类方法
public function send(...)
{
...
}
}
// 定义评论类
class Comment extend yii\db\ActiveRecord
{
// 用于引用发送邮件的库
private $_eMailSender;
// 初始化时,实例化 $_eMailSender
public function init()
{
...
// 这里假设使用Gmail的邮件服务
$this->_eMailSender = GmailSender::getInstance();
...
}
// 当有新的评价,即 save() 方法被调用之后中,会触发以下方法
public function afterInsert()
{
...
//
$this->_eMailSender->send(...);
...
}
}
|
上面的代码只是一个示意,大致是这么个流程。
那么这种常见的设计方法有什么问题呢? 主要问题在于 Comment 对于 GmailSender 的依赖(对于EmailSenderInterface的依赖不可避免), 假设有一天突然不使用Gmail提供的服务了,改用Yahoo或自建的邮件服务了。 那么,你不得不修改 Comment::init() 里面对 $_eMailSender 的实例化语句:
$this->_eMailSender = MyEmailSender::getInstance();
这个问题的本质在于,你今天写完这个Comment,只能用于这个项目,哪天你开发别的项目要实现类似的功能, 你还要针对新项目使用的邮件服务修改这个Comment。代码的复用性不高呀。 有什么办法可以不改变Comment的代码,就能扩展成对各种邮件服务都支持么? 换句话说,有办法将Comment和GmailSender解耦么?有办法提高Comment的普适性、复用性么?
依赖注入就是为了解决这个问题而生的,当然,DI也不是唯一解决问题的办法,毕竟条条大路通罗马。 Service Locator也是可以实现解耦的。
在Yii中使用DI解耦,有2种注入方式:构造函数注入、属性注入。
构造函数注入通过构造函数的形参,为类内部的抽象单元提供实例化。 具体的构造函数调用代码,由外部代码决定。具体例子如下:
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 |
// 这是构造函数注入的例子
class Comment extend yii\db\ActiveRecord
{
// 用于引用发送邮件的库
private $_eMailSender;
// 构造函数注入
public function __construct($emailSender)
{
...
$this->_eMailSender = $emailSender;
...
}
// 当有新的评价,即 save() 方法被调用之后中,会触发以下方法
public function afterInsert()
{
...
//
$this->_eMailSender->send(...);
...
}
}
// 实例化两种不同的邮件服务,当然,他们都实现了EmailSenderInterface
sender1 = new GmailSender();
sender2 = new MyEmailSender();
// 用构造函数将GmailSender注入
$comment1 = new Comment(sender1);
// 使用Gmail发送邮件
$comment1.save();
// 用构造函数将MyEmailSender注入
$comment2 = new Comment(sender2);
// 使用MyEmailSender发送邮件
$comment2.save();
|
上面的代码对比原来的代码,解决了Comment类对于GmailSender等具体类的依赖,通过构造函数,将相应的实现了 EmailSenderInterface接口的类实例传入Comment类中,使得Comment类可以适用于不同的邮件服务。 从此以后,无论要使用何何种邮件服务,只需写出新的EmailSenderInterface实现即可, Comment类的代码不再需要作任何更改,多爽的一件事,扩展起来、测试起来都省心省力。
与构造函数注入类似,属性注入通过setter或public成员变量,将所依赖的单元注入到类内部。 具体的属性写入,由外部代码决定。具体例子如下:
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 |
// 这是属性注入的例子
class Comment extend yii\db\ActiveRecord
{
// 用于引用发送邮件的库
private $_eMailSender;
// 定义了一个 setter()
public function setEmailSender($value)
{
$this->_eMailSender = $value;
}
// 当有新的评价,即 save() 方法被调用之后中,会触发以下方法
public function afterInsert()
{
...
//
$this->_eMailSender->send(...);
...
}
}
// 实例化两种不同的邮件服务,当然,他们都实现了EmailSenderInterface
sender1 = new GmailSender();
sender2 = new MyEmailSender();
$comment1 = new Comment;
// 使用属性注入
$comment1->eMailSender = sender1;
// 使用Gmail发送邮件
$comment1.save();
$comment2 = new Comment;
// 使用属性注入
$comment2->eMailSender = sender2;
// 使用MyEmailSender发送邮件
$comment2.save();
|
上面的Comment如果将 private $_eMailSender 改成 public $eMailSender 并删除 setter函数, 也是可以达到同样的效果的。
与构造函数注入类似,属性注入也是将Comment类所依赖的EmailSenderInterface的实例化过程放在Comment类以外。 这就是依赖注入的本质所在。为什么称为注入?从外面把东西打进去,就是注入。什么是外,什么是内? 要解除依赖的类内部就是内,实例化所依赖单元的地方就是外。
从上面DI两种注入方式来看,依赖单元的实例化代码是一个重复、繁琐的过程。 可以想像,一个Web应用的某一组件会依赖于若干单元,这些单元又有可能依赖于更低层级的单元, 从而形成依赖嵌套的情形。那么,这些依赖单元的实例化、注入过程的代码可能会比较长,前后关系也需要特别地注意, 必须将被依赖的放在需要注入依赖的前面进行实例化。 这实在是一件既没技术含量,又吃力不出成果的工作,这类工作是高智商(懒)人群的天敌, 我们是不会去做这么无聊的事情的。
就像极其不想洗衣服的人发明了洗衣机(我臆想的,未考证)一样,为了解决这一无聊的问题,DI容器被设计出来了。 Yii的DI容器是 yii\di\Container ,这个容器继承了发明人的高智商, 他知道如何对对象及对象的所有依赖,和这些依赖的依赖,进行实例化和配置。
容器顾名思义是用来装东西的,DI容器里面的东西是什么呢?Yii使用 yii\di\Instance 来表示容器中的东西。 当然Yii中还将这个类用于Service Locator,这个在讲Service Locator时再具体谈谈。
yii\di\Instance 本质上是DI容器中对于某一个类实例的引用,它的代码看起来并不复杂:
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 |
class Instance
{
// 仅有的属性,用于保存类名、接口名或者别名
public $id;
// 构造函数,仅将传入的ID赋值给 $id 属性
protected function __construct($id)
{
}
// 静态方法创建一个Instance实例
public static function of($id)
{
return new static($id);
}
// 静态方法,用于将引用解析成实际的对象,并确保这个对象的类型
public static function ensure($reference, $type = null, $container = null)
{
}
// 获取这个实例所引用的实际对象,事实上它调用的是
// yii\di\Container::get()来获取实际对象
public function get($container = null)
{
}
}
|
对于 yii\di\Instance ,我们要了解:
在DI容器中,维护了5个数组,这是DI容器功能实现的基础:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 用于保存单例Singleton对象,以对象类型为键
private $_singletons = [];
// 用于保存依赖的定义,以对象类型为键
private $_definitions = [];
// 用于保存构造函数的参数,以对象类型为键
private $_params = [];
// 用于缓存ReflectionClass对象,以类名或接口名为键
private $_reflections = [];
// 用于缓存依赖信息,以类名或接口名为键
private $_dependencies = [];
|
DI容器的5个数组内容和作用如 DI容器5个数组示意图 所示。
使用DI容器,首先要告诉容器,类型及类型之间的依赖关系,声明一这关系的过程称为注册依赖。 使用 yii\di\Container::set() 和 yii\di\Container::setSinglton() 可以注册依赖。 DI容器是怎么管理依赖的呢?要先看看 yii\di\Container::set() 和 yii\Container::setSinglton()
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 |
public function set($class, $definition = [], array $params = [])
{
// 规范化 $definition 并写入 $_definitions[$class]
$this->_definitions[$class] = $this->normalizeDefinition($class,
$definition);
// 将构造函数参数写入 $_params[$class]
$this->_params[$class] = $params;
// 删除$_singletons[$class]
unset($this->_singletons[$class]);
return $this;
}
public function setSingleton($class, $definition = [], array $params = [])
{
// 规范化 $definition 并写入 $_definitions[$class]
$this->_definitions[$class] = $this->normalizeDefinition($class,
$definition);
// 将构造函数参数写入 $_params[$class]
$this->_params[$class] = $params;
// 将$_singleton[$class]置为null,表示还未实例化
$this->_singletons[$class] = null;
return $this;
}
|
这两个函数功能类似没有太大区别,只是 set() 用于在每次请求时构造新的实例返回, 而setSingleton() 只维护一个单例,每次请求时都返回同一对象。
表现在数据结构上,就是 set() 在注册依赖时,会把使用 setSingleton() 注册的依赖删除。 否则,在解析依赖时,你让Yii究竟是依赖续弦还是原配?因此,在DI容器中,依赖关系的定义是唯一的。 后定义的同名依赖,会覆盖前面定义好的依赖。
从形参来看,这两个函数的 $class 参数接受一个类名、接口名或一个别名,作为依赖的名称。$definition 表示依赖的定义,可以是一个类名、配置数组或一个PHP callable。
这两个函数,本质上只是将依赖的有关信息写入到容器的相应数组中去。 在 set() 和setSingleton() 中,首先调用 yii\di\Container::normalizeDefinition() 对依赖的定义进行规范化处理,其代码如下:
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 |
protected function normalizeDefinition($class, $definition)
{
// $definition 是空的转换成 [‘class‘ => $class] 形式
if (empty($definition)) {
return [‘class‘ => $class];
// $definition 是字符串,转换成 [‘class‘ => $definition] 形式
} elseif (is_string($definition)) {
return [‘class‘ => $definition];
// $definition 是PHP callable 或对象,则直接将其作为依赖的定义
} elseif (is_callable($definition, true) || is_object($definition)) {
return $definition;
// $definition 是数组则确保该数组定义了 class 元素
} 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));
}
}
|
规范化处理的流程如下:
总之,对于 $_definitions 数组中的元素,它要么是一个包含了”class” 元素的数组,要么是一个PHP callable, 再要么就是一个具体对象。这就是规范化后的最终结果。
在调用 normalizeDefinition() 对依赖的定义进行规范化处理后, set() 和 setSingleton() 以传入的 $class 为键,将定义保存进 $_definition[] 中, 将传入的 $param 保存进 $_params[] 中。
对于 set() 而言,还要删除 $_singleton[] 中的同名依赖。 对于 setSingleton() 而言,则要将$_singleton[] 中的同名依赖设为 null , 表示定义了一个Singleton,但是并未实现化。
这么讲可能不好理解,举几个具体的依赖定义及相应数组的内容变化为例,以加深理解:
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 |
$container = new \yii\di\Container;
// 直接以类名注册一个依赖,虽然这么做没什么意义。
// $_definition[‘yii\db\Connection‘] = ‘yii\db\Connetcion‘
$container->set(‘yii\db\Connection‘);
// 注册一个接口,当一个类依赖于该接口时,定义中的类会自动被实例化,并供
// 有依赖需要的类使用。
// $_definition[‘yii\mail\MailInterface‘, ‘yii\swiftmailer\Mailer‘]
$container->set(‘yii\mail\MailInterface‘, ‘yii\swiftmailer\Mailer‘);
// 注册一个别名,当调用$container->get(‘foo‘)时,可以得到一个
// yii\db\Connection 实例。
// $_definition[‘foo‘, ‘yii\db\Connection‘]
$container->set(‘foo‘, ‘yii\db\Connection‘);
// 用一个配置数组来注册一个类,需要这个类的实例时,这个配置数组会发生作用。
// $_definition[‘yii\db\Connection‘] = [...]
$container->set(‘yii\db\Connection‘, [
‘dsn‘ => ‘mysql:host=127.0.0.1;dbname=demo‘,
‘username‘ => ‘root‘,
‘password‘ => ‘‘,
‘charset‘ => ‘utf8‘,
]);
// 用一个配置数组来注册一个别名,由于别名的类型不详,因此配置数组中需要
// 有 class 元素。
// $_definition[‘db‘] = [...]
$container->set(‘db‘, [
‘class‘ => ‘yii\db\Connection‘,
‘dsn‘ => ‘mysql:host=127.0.0.1;dbname=demo‘,
‘username‘ => ‘root‘,
‘password‘ => ‘‘,
‘charset‘ => ‘utf8‘,
]);
// 用一个PHP callable来注册一个别名,每次引用这个别名时,这个callable都会被调用。
// $_definition[‘db‘] = function(...){...}
$container->set(‘db‘, function ($container, $params, $config) {
return new \yii\db\Connection($config);
});
// 用一个对象来注册一个别名,每次引用这个别名时,这个对象都会被引用。
// $_definition[‘pageCache‘] = anInstanceOfFileCache
$container->set(‘pageCache‘, new FileCache);
|
setSingleton() 对于 $_definition 和 $_params 数组产生的影响与 set() 是一样一样的。 不同之处在于,使用 set() 会unset $_singltons 中的对应元素,Yii认为既然你都调用 set() 了,说明你希望这个依赖不再是单例了。 而 setSingleton() 相比较于 set() ,会额外地将$_singletons[$class] 置为 null 。 以此来表示这个依赖已经定义了一个单例,但是尚未实例化。
从 set() 和 setSingleton() 来看, 可能还不容易理解DI容器,比如我们说DI容器中维护了5个数组,但是依赖注册过程只涉及到其中3个。 剩下的 $_reflections 和 $_dependencies 是在解析依赖的过程中完成构建的。
从DI容器的5个数组来看也好,从容器定义了 set() 和 setSingleton() 两个定义依赖的方法来看也好, 不难猜出DI容器中装了两类实例,一种是单例,每次向容器索取单例类型的实例时,得到的都是同一个实例; 另一类是普通实例,每次向容器索要普通类型的实例时,容器会根据依赖信息创建一个新的实例给你。
单例类型主要用于节省构建实例的时间、节省保存实例的内存、共享数据等。而普通类型主要用于避免数据冲突。
对象的实例化过程要比依赖的定义过程复杂得多。毕竟依赖的定义只是往特定的数据结构$_singletons $_definitions 和 $_params 3个数组写入有关的信息。 稍复杂的东西也就是定义的规范化处理了。其它真没什么复杂的。像你这么聪明的,肯定觉得这太没挑战了。
而对象的实例化过程要相对复杂,这一过程会涉及到复杂依赖关系的解析、涉及依赖单元的实例化等过程。 且让我们抽丝剥茧地进行分析。
容器在获取实例之前,必须解析依赖信息。 这一过程会涉及到DI容器中尚未提到的另外2个数组$_reflections 和 $_dependencies 。 yii\di\Container::getDependencies() 会向这2个数组写入信息,而这个函数又会在创建实例时,由 yii\di\Container::build() 所调用。 如它的名字所示意的,yii\di\Container::getDependencies() 方法用于获取依赖信息,让我们先来看看这个函数的代码
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 |
protected function getDependencies($class)
{
// 如果已经缓存了其依赖信息,直接返回缓存中的依赖信息
if (isset($this->_reflections[$class])) {
return [$this->_reflections[$class], $this->_dependencies[$class]];
}
$dependencies = [];
// 使用PHP5 的反射机制来获取类的有关信息,主要就是为了获取依赖信息
$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();
// 构造函数没有默认值,则为其创建一个引用。
// 就是前面提到的 Instance 类型。
$dependencies[] = Instance::of($c === null ? null :
$c->getName());
}
}
}
// 将 ReflectionClass 对象缓存起来
$this->_reflections[$class] = $reflection;
// 将依赖信息缓存起来
$this->_dependencies[$class] = $dependencies;
return [$reflection, $dependencies];
}
|
前面讲了 $_reflections 数组用于缓存 ReflectionClass 实例,$_dependencies 数组用于缓存依赖信息。 这个 yii\di\Container::getDependencies() 方法实质上就是通过PHP5 的反射机制, 通过类的构造函数的参数分析他所依赖的单元。然后统统缓存起来备用。
为什么是通过构造函数来分析其依赖的单元呢? 因为这个DI容器设计出来的目的就是为了实例化对象及该对象所依赖的一切单元。 也就是说,DI容器必然构造类的实例,必然调用构造函数,那么必然为构造函数准备并传入相应的依赖单元。 这也是我们开头讲到的构造函数依赖注入的后续延伸应用。
可能有的读者会问,那不是还有setter注入么,为什么不用解析setter注入函数的依赖呢? 这是因为要获取实例不一定需要为某属性注入外部依赖单元,但是却必须为其构造函数的参数准备依赖的外部单元。 当然,有时候一个用于注入的属性必须在实例化时指定依赖单元。 这个时候,必然在其构造函数中有一个用于接收外部依赖单元的形式参数。 使用DI容器的目的是自动实例化,只是实例化而已,就意味着只需要调用构造函数。 至于setter注入可以在实例化后操作嘛。
另一个与解析依赖信息相关的方法就是 yii\di\Container::resolveDependencies() 。 它也是关乎$_reflections 和 $_dependencies 数组的,它使用 yii\di\Container::getDependencies() 在这两个数组中写入的缓存信息,作进一步具体化的处理。从函数名来看,他的名字表明是用于解析依赖信息的。 下面我们来看看它的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
protected function resolveDependencies($dependencies, $reflection = null)
{
foreach ($dependencies as $index => $dependency) {
// 前面getDependencies() 函数往 $_dependencies[] 中
// 写入的是一个 Instance 数组
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {
// 向容器索要所依赖的实例,递归调用 yii\di\Container::get()
$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;
}
|
上面的代码中可以看到, yii\di\Container::resolveDependencies() 作用在于处理依赖信息, 将依赖信息中保存的Istance实例所引用的类或接口进行实例化。
综合上面提到的 yii\di\Container::getDependencies() 和 yii\di\Container::resolveDependencies()两个方法,我们可以了解到:
解析完依赖信息,就万事俱备了,那么东风也该来了。实例的创建,秘密就在yii\di\Container::build() 函数中:
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 |
protected function build($class, $params, $config)
{
// 调用上面提到的getDependencies来获取并缓存依赖信息,留意这里 list 的用法
list ($reflection, $dependencies) = $this->getDependencies($class);
// 用传入的 $params 的内容补充、覆盖到依赖信息中
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
// 这个语句是两个条件:
// 一是要创建的类是一个 yii\base\Object 类,
// 留意我们在《Yii基础》一篇中讲到,这个类对于构造函数的参数是有一定要求的。
// 二是依赖信息不为空,也就是要么已经注册过依赖,
// 要么为build() 传入构造函数参数。
if (!empty($dependencies) && is_a($class, ‘yii\base\Object‘, true)) {
// 按照 Object 类的要求,构造函数的最后一个参数为 $config 数组
$dependencies[count($dependencies) - 1] = $config;
// 解析依赖信息,如果有依赖单元需要提前实例化,会在这一步完成
$dependencies = $this->resolveDependencies($dependencies, $reflection);
// 实例化这个对象
return $reflection->newInstanceArgs($dependencies);
} else {
// 会出现异常的情况有二:
// 一是依赖信息为空,也就是你前面又没注册过,
// 现在又不提供构造函数参数,你让Yii怎么实例化?
// 二是要构造的类,根本就不是 Object 类。
$dependencies = $this->resolveDependencies($dependencies, $reflection);
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}
|
从这个 yii\di\Container::build() 来看:
与注册依赖时使用 set() 和 setSingleton() 对应,获取依赖实例化对象使用yii\di\Container::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 |
public function get($class, $params = [], $config = [])
{
// 已经有一个完成实例化的单例,直接引用这个单例
if (isset($this->_singletons[$class])) {
return $this->_singletons[$class];
// 是个尚未注册过的依赖,说明它不依赖其他单元,或者依赖信息不用定义,
// 则根据传入的参数创建一个实例
} elseif (!isset($this->_definitions[$class])) {
return $this->build($class, $params, $config);
}
// 注意这里创建了 $_definitions[$class] 数组的副本
$definition = $this->_definitions[$class];
// 依赖的定义是个 PHP callable,调用之
if (is_callable($definition, true)) {
$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;
}
|
get() 用于返回一个对象或一个别名所代表的对象。可以是已经注册好依赖的,也可以是没有注册过依赖的。 无论是哪种情况,Yii均会自动解析将要获取的对象对外部的依赖。
get() 接受3个参数:
get() 解析依赖获取对象是一个自动递归的过程,也就是说,当要获取的对象依赖于其他对象时, Yii会自动获取这些对象及其所依赖的下层对象的实例。 同时,即使对于未定义的依赖,DI容器通过PHP的Reflection API,也可以自动解析出当前对象的依赖来。
get() 不直接实例化对象,也不直接解析依赖信息。而是通过 build() 来实例化对象和解析依赖。
get() 会根据依赖定义,递归调用自身去获取依赖单元。 因此,在整个实例化过程中,一共有两个地方会产生递归:一是 get() , 二是 build() 中的 resolveDependencies() 。
DI容器解析依赖实例化对象过程大体上是这么一个流程:
从 get() 的代码可以看出:
为了加深理解,我们以官方文档上的例子来说明DI容器解析依赖的过程。假设有以下代码:
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 |
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);
}
}
|
从依赖关系看,这里的 UserLister 类依赖于接口 UserFinderInterface , 而接口有一个实现就是UserFinder 类,但这类又依赖于 Connection 。
那么,按照一般常规的作法,要实例化一个 UserLister 通常这么做:
$db = new \yii\db\Connection([‘dsn‘ => ‘...‘]);
$finder = new UserFinder($db);
$lister = new UserLister($finder);
就是逆着依赖关系,从最底层的 Connection 开始实例化,接着是 UserFinder 最后是 UserLister。 在写代码的时候,这个前后顺序是不能乱的。而且,需要用到的单元,你要自己一个一个提前准备好。 对于自己写的可能还比较清楚,对于其他团队成员写的,你还要看他的类究竟是依赖了哪些,并一一实例化。 这种情况,如果是个别的、少量的还可以接受,如果有个10-20个的,那就麻烦了。 估计光实例化的代码,就可以写满一屏幕了。
而且,如果是团队开发,有些单元应当是共用的,如邮件投递服务。 不能说你写个模块,要用到邮件服务了,就自己实例化一个邮件服务吧?那样岂不是有N模块就有N个邮件服务了? 最好的方式是使邮件服务成为一个单例,这样任何模块在需要邮件服务时,使用的其实是同一个实例。 用传统的这种实例化对象的方法来实现的话,就没那么直接了。
那么改成DI容器的话,应该是怎么样呢?他是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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‘);
|
采用DI容器的办法,首先各 set() 语句没有前后关系的要求, set() 只是写入特定的数据结构, 并未涉及具体依赖关系的解析。所以,前后关系不重要,先定义什么依赖,后定义什么依赖没有关系。
其次,上面根本没有在DI容器中定义 UserFinder 对于 Connection 的依赖。 但是DI容器通过对UserFinder 构造函数的分析,能了解到这个类会对 Connection 依赖。这个过程是自动的。
最后,上面只有一个 get() 看起来好像根本没有实例化其他如 Connection 单元一样,但事实上,DI容器已经安排好了一切。 在获取 userLister 之前, Connection 和 UserFinder 都会被自动实例化。 其中, Connection 是根据依赖定义中的配置数组进行实例化的。
经过上面的几个 set() 语句之后,DI容器的 $_params 数组是空的, $_singletons 数组也是空的。$_definintions 数组却有了新的内容:
1 2 3 4 5 6 7 8 |
$_definitions = [
‘yii\db\Connection‘ => [
‘class‘ => ‘yii\db\Connection‘, // 注意这里
‘dsn‘ => ...
],
‘app\models\UserFinderInterface‘ => [‘class‘ => ‘app\models\UserFinder‘],
‘userLister‘ => [‘class‘ => ‘app\models\UserLister‘] // 注意这里
];
|
在调用 get(‘userLister‘) 过程中又发生了什么呢?说实话,这个过程不是十分复杂, 但是由于涉及到递归和回溯,写这里的时候,我写了改,改了写,示意图画了好几回,折腾了好久,都不满意, 就怕说不清楚,读者朋友们理解起来费劲。 最后画了一个简单的示意图,请你们对照 DI容器解析依赖获取实例的过程示意图 , 以及前面关于 get() build() getDependencies()resolveDependencies() 等函数的源代码, 了解大致流程。如果有任何疑问、建议,也请在底部留言。
在 DI容器解析依赖获取实例的过程示意图 中绿色方框表示DI容器的5个数组,浅蓝色圆边方框表示调用的函数和方法。 蓝色箭头表示读取内存,红色箭头表示写入内存,虚线箭头表示参照的内存对象,粗线绿色箭头表示回溯过程。 图中3个圆柱体表示实例化过程中,创建出来的3个实例。
对于 get() 函数:
build() 在实例化过程中,干了这么几件事:
getDependencies() 函数总是被 build() 调用,他干了这么几件事:
resolveDependencies() 函数总是被 build() 调用,他在实例化时,干了这么几件事:
newInstanceArgs() 函数是PHP Reflection API的函数,用于创建实例,具体请看 PHP手册 。
这里只是简单的举例子而已,还没有涉及到多依赖和单例的情形,但是在原理上是一样的。 希望继续深入了解的读者朋友可以再看看上面有关函数的源代码就行了,有疑问请随时留言。
从上面的例子中不难发现,DI容器维护了两个缓存数组 $_reflections 和 $_dependencies 。这两个数组只写入一次,就可以无限次使用。 因此,减少了对ReflectionClass的使用,提高了DI容器解析依赖和获取实例的效率。
另一方面,我们看到,获取一个实例,步骤其实不少。但是,对于典型的Web应用而言, 有许多模块其实应当注册为单例的,比如上面的 yii\db\Connection 。 一个Web应用一般使用一个数据库连接,特殊情况下会用多几个,所以这些数据库连接一般是给定不同别名加以区分后, 分别以单例形式放在容器中的。因此,实际获取实例时,步骤会简单得。对于单例, 在第一次 get() 时,直接就返回了。而且,省去不重复构造实例的过程。
这两个方面,都体现出Yii高效能的特点。
上面我们分析了DI容器,这只是其中的原理部分,具体的运用,我们将结合 服务定位器(Service Locator) 来讲。
标签:
原文地址:http://www.cnblogs.com/liuwenbohhh/p/4414737.html