在Web应用中,很常见的是使用各种第三方Web Service实现特定功能,比如发送邮件、推送微博等。假设要实现当访客在博客上发表评论后,向博文的作者发送Email的功能,通常代码会这样写:
// 为邮件服务定义抽象层
interface EmailSenderInterface{
public function send(){}
}
// 定义Gmail邮件服务
class GmailSender implments 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的普适性、复用性么?
依赖注入就是为了解决这个问题而生的!
构造函数注入
构造函数注入通过构造函数的形参,为类内部的抽象单元提供实例化。具体的构造函数调用代码,由外部代码决定。具体例子如下:
// 这是构造函数注入的例子
class Comment extend yii\db\ActiveRecord{
// 用于引用发送邮件的库
private $_eMailSender;
// 构造函数注入
public function __construct($emailSender){
$this->_eMailSender = $emailSender;
}
// 当有新的评论,即save()方法被调用之后,会触发以下方法
public function afterInsert(){
$this->_eMailSender->send();
}
}
// 实例化2种不同的邮件服务,当然,它们都实现了EmailSenderInterface
sender1 = new GmailSender();
sender2 = new MyEmailSender();
// 用构造函数将GmailSender注入
$comment1 = new Comment(sender1);
$comment1.save(); //使用Gmail发送邮件
// 用构造函数将MyEmailSender注入
$comment2 = new Comment(sender2);
$comment2.save(); //使用MyEmailSender发送邮件
上面的代码对比原来的代码,解决了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;
}
public function afterInsert(){
$this->_eMailSender->send();
}
}
// 实例化2种不同的邮件服务,当然,它们都实现了EmailSenderInterface
sender1 = new GmailSender();
sender2 = new MyEmailSender();
$comment1 = new Comment();
$comment1->emailSender = sender1; //使用属性注入
$comment1.save();
$comment2 = new Comment();
$comment2->emailSender = sender2;//使用属性注入
$comment2.save();
上面的Comment如果将private $_eMailSender
改成 public $eMailSender
并删除 setter函数, 也是可以达到同样的效果的。
与构造函数注入类似,属性注入也是将Comment类所依赖的EmailSenderInterface的实例化过程放在Comment类以外。 这就是依赖注入的本质所在。为什么称为注入?从外面把东西打进去,就是注入。什么是外,什么是内? 要解除依赖的类内部就是内,实例化所依赖单元的地方就是外。