依赖注入模式及其容器的简单实现

依赖注入(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库。示例项目中的代码可以轻松扩展以从配置文件注册依赖项。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485