传统软件设计中,上层代码依赖于下层代码,当下层出现变更时,上层也要相应变化。html
DIP的核心思想是:上层定义接口,下层实现这个接口,从而使的下层依赖于上层,下降耦合。web
IoC是DIP的具体思路作法,IoC的核心是将类所依赖的下层单元的实例化过程交由第三方来实现。数据库
一个简单的特征:类中不对所依赖的单元有诸如$component = new yii\component\SomeClass()
的实例化语句。swift
DI是IoC的一种设计模式。设计模式
DI的核心是把类所依赖的单元的实例化过程,放到类的外面去实现。数组
当项目比较大时,依赖关系可能很复杂。而IoCC提供了动态地建立、注入依赖单元,映射依赖关系等功能。Yii设计了一个yii\di\Container
来实现了DI Container。缓存
SL时IoC的另外一种实现方式,其核心是把全部可能用到的依赖单元交给SL进行实例化和操做,把类对依赖单元的依赖,转换成类对SL的依赖。数据结构
Yii2经过DI容器,实现了SL。app
DI在web中,常见于使用第三方服务实现特定功能(例:发邮件,推微博)。框架
假设要实现当访客在博客上发表评论后,向博文的做者发送Email的功能,一般代码以下:
// 为邮件服务定义抽象层 interface EmailSenderInterface{ public function send(); } // 定义Gmail服务 class GmailSender implements EmailSenderInterface{ public function send() } // 定义评论类 class Comment extend yii\db\ActiveRecord{ private $_eEmailSender; public function init(){ $this->_eMailSender = GmailSender::getInstance(); } public function afterInsert(){ $this->_eMailSender->send(); } }
这个常见的设计方法有一个问题:Comment对于GmailSender的依赖,忽然有一天不用Gmail了,那么必须修改init里的实例化语句。
同时,这个类的复用程度不高,下一个不用Gmail服务的项目,还须要再修改,或者直接去掉该邮件服务。
在Yii中使用DI解耦,有两种注入方式:构造函数注入、属性注入。
class Comment extend yii\db\ActiveRecord{ private $_eMailSender; public function __construct($emailSender){ $this->_eMailSender = $emailSender; } public function afterInsert(){ $this->_eMailSender->send(); } } // 实例化两种不一样的邮件服务,都继承了基类 $sender1 = new GmailSender(); $sender2 = new MyEmailSender(); $comment1 = new Comment($sender1); $comment1.save(); $comment2 = new Comment($sender2); $comment2.save();
class Comment extend yii\db\ActiveRecord{ private $_eMailSender; public function setEmailSender($value){ $this->_eMailSender = $value; } public function afterInsert(){ $this->_eMailSender->send(); } }
实际上,依赖注入就是从外面,将实例打到内部,从而完成总体的功能。
打入的方式有两种,一种是初始化是经过传参。另一种是调用内部set
方法,将实例注入属性,内部方法会调用该属性,进而完成功能。
一个Web应用的某一组件会依赖于若干单元,这些单元又有可能依赖于基本单元,从而造成依赖嵌套的情形。
那么,这些依赖单元的实例化、注入过程的代码就会又长又繁杂,先后关系也须要注意。
yii\di\Container
,经过DI容器,能够更好的管理对象及对象的全部依赖,以及这些依赖的依赖,进行实例化和配置。
Yii使用yii\di\Instance
来表示容器中的东西。Yii还将这个类用于Service Locator。
该Instance
本质上是DI容器中对于某一个类实例的引用,它的代码看起来并不复杂:
class Instance{ // 保存类名,借口名,别名 public $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){} }
该Instance:
表示的是容器中的内容,表明的是对于实际对象的引用。
DI容器能够经过他获取所引用的实际对象。
属性id表示实例的类型.
// 用于保存单例Singleton对象,以对象类型为键 private $_singletons = []; // 用于保存依赖的定义,以对象类型为键 private $_definitions = []; // 用于保存构造函数的参数,以对象类型为键 private $_params = []; // 用于缓存ReflectionClass对象,以类名或接口名为键 private $_reflections = []; // 用于缓存依赖信息,以类名或接口名为键 private $_dependencies = [];
使用DI容器,首先要告诉容器,类型及类型之间的依赖关系,声明这一关系的过程称为注册依赖
。
使用:yii\di\Container::set()
& yii\di\Container::setSinglton()
在DI容器中,依赖关系的定义是惟一的。 后定义的同名依赖,会覆盖前面定义好的依赖。
对于 set() 而言,还要删除 $_singleton[] 中的同名依赖。 对于 setSingleton() 而言,则要将 $_singleton[] 中的同名依赖设为 null , 表示定义了一个Singleton,可是并未实现化。
$container = new \yii\di\Container; // 直接以一个类名注册一个依赖 // $_definition['\yii\db\Connection'] = '\yii\db\Connection'; $container->set('\yii\db\Connection'); // 注册一个接口,当一个类依赖于该接口时,定义中的类会自动被实例化,并给有依赖须要的类使用 // $_definition['yii\mail\MailInterface'] = 'yii\swiftmailer\Mailer'; $container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer'); // 注册一个别名 $container->set('foo', 'yii\db\Connection'); // 用callable来注册一个别名,每次引用这个别名时,该callable都会被调用 $container->set('db', function($container, $params, $config){ return new \yii\db\Connectin($config); });
你能够这么理解:依赖的定义只是往特定的数据结构中写入有关的信息。
DI容器中装了两类实例,一种是单例,每次向容器索取单例类型的实例时,获得的都是同一个实例; 另外一类是普通实例,每次向容器索要普通类型的实例时,容器会根据依赖信息建立一个新的实例给你。
yii\di\Container::getDependencies()
该方法实质上就是经过PHP5的反射机制,经过类的构造函数的参数分析他所依赖的单元。而后通通缓存起来备用。
另外一个与解析依赖信息相关的方法就是 yii\di\Container::resolveDependencies()
。
$_reflections
以类(接口、别名)名为键, 缓存了这个类(接口、别名)的ReflcetionClass
。一经缓存,便不会再更改。
$_dependencies
以类(接口、别名)名为键,缓存了这个类(接口、别名)的依赖信息。
这两个缓存数组都是在yii\di\Container::getDependencies()
中完成。这个函数只是简单地向数组写入数据。
通过yii\di\Container::resolveDependencies()
处理,DI容器会将依赖信息转换成实例。 这个实例化的过程当中,是向容器索要实例。也就是说,有可能会引发递归。
yii\di\Container::build()
:
DI容器只支持yii\base\Object
类。也就是说,你只能向DI容器索要 yii\base\Object 及其子类。 再换句话说,若是你想你的类能够放在DI容器里,那么必须继承自 yii\base\Object 类。 但Yii中几乎开发者在开发过程当中须要用到的类,都是继承自这个类。 一个例外就是上面提到的 yii\di\Instance 类。但这个类是供Yii框架本身使用的,开发者无需操做这个类。
递归获取依赖单元的依赖在于dependencies = $this->resolveDependencies($dependencies, $reflection)
中。
getDependencies() 和 resolveDependencies() 为 build() 所用。 也就是说,只有在建立实例的过程当中,DI容器才会去解析依赖信息、缓存依赖信息。
获取依赖实例化对象使用yii\di\Container::get()
,
在整个实例化过程当中,一共有两个地方会产生递归:一是 get() , 二是 build() 中的 resolveDependencies() 。
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); } pubic function findUser(){} } class UserLister extends Object{ public $finder; // 依赖接口 public function __construct(UserFinderInterface $finder, $config = []){ $this->finder = $finder; parent::__construct($config); } }
通常作法:
$db = new \yii\db\Connection(['dsn' => '...']); $finder = new UserFinder($db); $lister = new UserLister($finder);
团队开发的时候,不少类须要制定为单例模式,不然N个模块有N个服务就。。
上部代码改为DI容器
use yii\di\Container; $container = new Container; $container->set('yii\db\Connection', [...]); $container->set('app\models\UserFinderInterface', [ 'class' => 'app\models\UserFinder', ]); $container->set('userLister', 'app\models\UserLister'); // 获取该别名class的实例 $lister = $container->get('userLister');
DI容器维护了两个缓存数组 $_reflections 和 $_dependencies 。这两个数组只写入一次,就能够无限次使用。 所以,减小了对ReflectionClass的使用,提升了DI容器解析依赖和获取实例的效率。
可是,对于典型的Web应用而言, 有许多模块其实应当注册为单例的,好比上面的 yii\db\Connection
。一个Web应用通常使用一个数据库链接,特殊状况下会用多几个,因此这些数据库链接通常是给定不一样别名加以区分后, 分别以单例形式放在容器中的。所以,实际获取实例时,步骤会简单得。对于单例, 在第一次get()
时,直接就返回了。并且,省去不重复构造实例的过程。
参考