依赖注入(Dependency Injection, DI)是一种设计模式,它通过将依赖关系传递给对象,而不是由对象自己创建依赖关系,从而实现控制反转(Inversion of Control, IoC)。这种模式使得代码更加灵活和可测试。在本文中,将探讨依赖注入模式的概念,并提供一个简单的依赖注入容器类,以便在PHP项目中使用。
依赖注入模式非常简单,通过一些代码示例,可以开始理解该模式希望实现的目标。以下是一个简单的PHP代码片段。
class Mailer {
private $transport;
private $emailTemplate;
public function __construct() {
$this->transport = new Smtp('host', 'port', 'user', 'pass');
$this->emailTemplate = file_get_contents('template.html');
}
public function send($from, $to, $subject) {
$this->transport->send($from, $to, $subject, $this->emailTemplate);
}
}
$mailer = new Mailer();
$mailer->send('Syed Hussain', 'john@domain-xyz.com', 'Marketing Email');
上述代码描述了一个简单的Mailer类,它加载一个HTML模板并将其发送到电子邮件地址。可以将其视为一种新闻通讯。这种类型的代码相当常见,因为它将实现包装到一个单独的类中,这在一定程度上可以重用。但是,如果打算将此类分发给同事,那么需要接受Mailer类只能使用SMTP发送电子邮件的事实,因为它已经被硬编码到类中。如果同事需要使用第三方API(如Mandrill或Mailgun)发送电子邮件,那么Mailer类将无法满足他们的需求,因为它使用了SMTP。他们可以替换SMTP代码以使用Mandrill的Web API。但是,如果他们随后决定从Mandrill更改为Mailgun或任何其他可以发送电子邮件的提供商,那么紧密耦合的实现不够灵活,无法允许使用不同类型的传输机制。
解决这个问题的方法非常简单。与其硬编码SMTP类,不如将其作为配置数据提供给Mailer类。这是通过创建SMTP类的实例并将其实例注入到Mailer类中来完成的,无论是通过Mailer的构造函数还是通过setter方法。以下代码显示了Mailer类的修订版本,它在类构造函数中接受一个SMTP对象实例。
class Mailer {
private $transport;
private $emailTemplate;
public function __construct($transport) {
$this->transport = $transport;
$this->emailTemplate = file_get_contents('template.html');
}
public function send($from, $to, $subject) {
$this->transport->send($from, $to, $subject, $this->emailTemplate);
}
}
class Smtp {
public function send($from, $to, $subject, $message) {
// 使用smtp发送消息。
}
}
$mailer = new Mailer(new Smtp());
$mailer->send('Syed Hussain', 'john@domain.com', 'Marketing Email');
代码的这一简单变化使得Mailer类更加灵活。它可以接收任何对象并尝试调用其send()方法。说尝试,因为类不检查提供的是否有send()方法,但通过使用接口可以很容易地纠正这一点。
这里的重点是Mailer类正在被提供其依赖项。Mailer类不能没有传输机制,否则Mailer将无用。
在最简单的层面上,依赖注入是向客户端(Mailer类)提供配置数据的能力,该客户端依赖于其他服务(Smtp类)。
可能已经在某个时候使用过依赖注入模式,无论是有意还是无意。依赖注入也被称为控制反转(IoC)。这个名字来自于客户端的控制已经被反转,意味着客户端不再负责其依赖项,控制权已经交给了另一个实体。
如果从客户端移除依赖项,那么意味着依赖项必须在另一个范围内创建。这里使用的术语范围是“其他地方”,并且这些依赖项可能有自己的依赖项。无论如何,所有依赖项必须在依赖项(客户端)可以使用它们之前存在。这有一个副作用,就是膨胀代码并引入错误。例如,可能知道某个类需要哪些依赖项,但同事可能不知道,并可能使用不同的依赖项,这可能导致错误。这就是依赖注入容器(DIC)主要使用的地方。DIC是一种实现,可以用任何编程语言实现。
在本节中,将解释附带的示例项目的使用以及一些代码示例。以下代码创建了一个DiContainer的实例并注册了一个依赖项。
$DiContainer->register('db', 'PDO')
->addArgument('mysql:host=localhost;dbname=db')
->addArgument('username')
->addArgument('password');
register方法注册一个依赖项,并接受两个参数。第一个参数是给依赖项的唯一名称。这是检索依赖项时使用的名称。第二个参数是依赖项的类名。上面的代码注册了一个名为'db'的依赖项,类名为'PDO'。
addArgument()方法用于添加PDO类构造函数所需的参数。请注意,代码不存储任何实例化的对象,只是对将被动态实例化的类名的引用。
依赖项注册后,可以通过以下三种不同的方式访问:
$db = $DiContainer->getInstance('db');
$db = $DiContainer->getSingleInstance('db');
$db = $DiContainer->db;
调用getInstance()方法并传入依赖项名称,将返回依赖项的新实例。调用getSingleInstance()方法将只返回一次实例。DiContainer将在内部维护此实例,并在后续对相同依赖项使用getSingleInstance()方法时返回它。最后一种方法允许使用依赖项名称作为属性。在内部,DiContainer将调用getInstance()方法。
在某些情况下,可能希望将注册的依赖项用作正在注册的另一个依赖项的参数。以下代码示例显示了如何解决这个问题。
class Mailer {
private $transport;
public function __construct($transport) {
$this->transport = $transport;
}
}
class Smtp {
public function __construct($host, $port, $user, $pass) {
// 处理参数
}
}
$DiContainer = new DiContainer();
$DiContainer->register('smtp', 'Smtp')
->addArgument('host')
->addArgument('port')
->addArgument('user')
->addArgument('pass');
$DiContainer->register('mailer', 'Mailer')
->addArgument('@@smtp');
$mailer = $DiContainer->getInstance('mailer');
注意依赖项smtp已被注册并赋予名称smtp。Mailer类需要将smtp实例传递到其构造函数。在注册Mailer依赖项时,使用了双@@后跟依赖项名称来引用之前注册的依赖项。
DIC的实现各不相同,每种编程语言或供应商都有自己的语法。一些DIC超越了将参数传递给类构造函数,甚至可以调用类方法并带有参数。
PHP的简单反射功能使得任何人都可以轻松地开发自己的DIC库。示例项目中的代码可以轻松扩展以从配置文件注册依赖项。