@原文url:http://www.digpage.com/di.html
https://my.oschina.net/cxz001/blog/227482?fromerr=eQovcbw9
理解什么是Di/IoC,依赖注入/控制反转。两者说的是一个东西,是当下流行的一种设计模式。大致的意思就是,准备一个盒子(容器),事先将项目中可能用到的类扔进去,在项目中直接从容器中拿,也就是避免了直接在项目中到处new,造成大量耦合。取而代之的是在项目类里面增设 setDi()和getDi()方法,通过Di同一管理类。
依赖注入(Denpdency Injection, DI)和依赖注入容器(DI容器)
为了降低代码耦合程度,提高项目的可维护性,Yii采用多许多当下最流行又相对成熟的设计模式,包括了依赖注入(Denpdency Injection, DI)和服务定位器(Service Locator)两种模式。这些设计模式对于提高自身的设计水平很有帮助,这也是我们学习Yii的一个重要出发点。
相关概念
依赖倒置原则(Dependence Inversion Principle, DIP)
DIP是一种软件设计的指导思想。传统软件设计中,上层代码依赖于下层代码,当下层出现变动时, 上层代码也要相应变化,维护成本较高。而DIP的核心思想是上层定义接口,下层实现这个接口, 从而使得下层依赖于上层,降低耦合度,提高整个系统的弹性。这是一种经实践证明的有效策略。
控制反转(Inversion of Control, IoC)
IoC就是DIP的一种具体思路,DIP只是一种理念、思想,而IoC是一种实现DIP的方法。 IoC的核心是将类(上层)所依赖的单元(下层)的实例化
过程交由第三方来实现。 一个简单的特征,就是类中不对所依赖的单元有诸如 $component = new yii\component\SomeClass() 的实例化语句。
依赖注入(Dependence Injection, DI)
DI是IoC的一种设计模式,是一种套路,按照DI的套路,就可以实现IoC,就能符合DIP原则。 DI的核心是把类所依赖的单元的实例化过程,放到类的外面去实现。
控制反转容器(IoC Container)
当项目比较大时,依赖关系可能会很复杂。 而IoC Container提供了动态地创建、注入依赖单元,映射依赖关系等功能,减少了许多代码量。 Yii 设计了
一个 yii\di\Container 来实现了 DI Container。
服务定位器(Service Locator)
Service Locator是IoC的另一种实现方式, 其核心是把所有可能用到的依赖单元交由Service Locator进行实例化和创建、配置, 把类对依赖单元的依赖,
转换成类对Service Locator的依赖。 DI 与 Service Locator并不冲突,两者可以结合使用。 目前,Yii2.0把这DI和Service Locator这两个东西结合起来使用,或
者说通过DI容器,实现了Service Locator。
一,依赖注入,首先讲讲DI
在Web应用中,很常见的是使用各种第三方Web Service实现特定的功能,比如发送邮件、推送微博等。 假设要实现当访客在博客上发表评论后,向博文的作者发送Email的功能,通常代码会是这样:
// 为邮件服务定义抽象层 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种注入方式:构造函数注入、属性注入。
构造函数注入
构造函数注入通过构造函数的形参,为类内部的抽象单元提供实例化。 具体的构造函数调用代码,由外部代码决定。具体例子如下:
// 这是构造函数注入的例子 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成员变量,将所依赖的单元注入到类内部。 具体的属性写入,由外部代码决定。具体例子如下:
// 这是属性注入的例子 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容器(以Yii Di容器为案例)
---蛋定龙php系列视屏教程-Yii2依赖注入容器
从上面DI两种注入方式来看,依赖单元的实例化代码是一个重复、繁琐的过程。 yii中一个Web应用的某一组件会依赖于若干单元,这些单元又有可能依赖于更低层级的单元, 从而形成依赖嵌套的情形。那么,这些依赖单元的实例化、注入过程的代码可能会比较长,前后关系也需要特别地注意, 必须将被依赖的类放在需要注入依赖的代码前面进行实例化。 这实在是一件既没技术含量,又吃力不出成果的工作,这类工作是高智商(懒)人群的天敌, 我们是不会去做这么无聊的事情的。怎么办?使用DI容器
,准备一个盒子(容器),事先将项目中可能用到的类扔进去,在项目中直接从容器中拿,也就是避免了直接在项目中到处new,造成大量的耦合,取而代之的是在项目类里面增设setDi()和getDi()方法,通过Di统一管理类。
---Yii容器提供依赖注入类型
---Yii Di容器主要函数
---set和setSingleton的区别
(一),视频相关代码
1,yii中的回调注入,例如下边代码:
public function actionCallbackdi(){
$container = new Container(); //声明一个yii-Di容器对象
//注册对象
$container -> set('Foo', function($container,$params,$config){
echo "<pre>";
$obj new Foo();
//这里很重要,设置依赖,$obj->Bar = get获取另外一个实例对象。
$obj -> Bar = $container -> get('dandinglong\containertest\Bar');
return $obj;
});
}
---类似于di-demo中的匿名函数注入,把$container对象当做函数参数注入,然后返回想要的对象。
2,Yii2中错误使用Di容器01---找不到类
public function actionErrordione(){
$container = new Container();
$aaa = $container->get('abc'); //没有set abc这个类,所以这里获取不到,会报Class abc does not exist
var_dump($aaa);
}
@总结(不用Di容器,用什么,读Spring有感?):
***可以参照di-demo案例或者yii案例来了解Di容器的使用
***阅读Struts2+Spring3+Hibemate整合开发,Spring对于Di容器的理解?
1,如果开发中不用Di容器,应用中各组件以怎样的方式耦合?
很多经验丰富的项目经理统一回答,用工厂模式,服务定位器模式等。(实现了部分spring功能)
2,,spring的核心机制:依赖注入(目前最优秀的解耦方式)
3,依赖注入,控制反转,其含义完全相同,依赖注入是因为控制反转太难理解,才重新起的名字。
Ioc容器=Di容器
4,所谓依赖注入,是指程序运行过程中,如果需要另一个对象协作(调用它的方法,访问它的属性)时,无须在代码中创建
被调用者,而是依赖于外部容器的注入。
5,编码的三种请求,三种境界
(1),第一种情况,java实例的调用者创建被调用的Java实例,调用者直接使用new创建被调用者实例,程序高度耦合,效率低下
,真实的应用及少使用这种方式。
(2),第二种情况,调用者无须关心被调用者的具体实现过程,只需要找到符合某种标准(接口)的实例,即可使用。此时调用的
代码面向接口编程,可以让调用者和被调用者解耦,这也是工厂模式大量使用的原因。但调用者需要主动定位工厂,调用者与
工厂耦合在一起。
(3),第三种情况,最理想的情况:程序完全无须理会被调用者的实现,也无须主动定位工厂,这是最好的解耦方式。实例之间
依赖关系由Ioc容器负责管理。