65.9K
CodeProject 正在变化。 阅读更多。
Home

一个简单的依赖注入容器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2014年4月12日

CPOL

6分钟阅读

viewsIcon

11582

downloadIcon

117

一个轻量级的 PHP 依赖注入容器

引言

依赖注入模式已经存在很长时间,并且在许多应用程序中被广泛使用。在本文中,我将解释什么是依赖注入模式,并介绍一个简单的依赖注入容器类,您可以在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(如MandrillMailgun)发送电子邮件,那么Mailer类将无法满足他们的要求,因为它使用Smtp。他们可以替换Smtp代码以使用Mandrill的Web API。但是,如果他们后来决定从Mandrill更改为Mailgun或任何其他可以发送电子邮件的提供商呢?这种紧耦合的实现不够灵活,无法使用不同类型的传输机制。

这个问题的解决方案其实很简单。不要硬编码Smtp类,只需将其作为配置数据提供给Mailer类即可。这可以通过创建Smtp类的实例,并通过Mailers构造函数或setter方法将其注入Mailer类来实现。下面的代码显示了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){
        // Send the message using smtp.
    }
}

$mailer = new Mailer(new Smtp());
$mailer->send('Syed Hussain', 'john@domain.com', 'Marketing Email');

代码中的这个简单更改使Mailer类更加灵活。它可以接受任何对象并尝试调用其send()方法。我说尝试是因为该类不检查提供的对象是否具有send()方法,但这可以通过使用接口轻松纠正。

这里的重点是Mailer类正在获得其**依赖项**。Mailer类在没有传输机制的情况下无法正常工作,这使得Mailer变得无用。

最简单地说,依赖注入是能够向依赖于其他服务(Smpt类)的客户端(Mailer类)提供配置数据的能力。

您可能已经在某个时候使用过依赖注入模式,无论是有意还是无意。依赖注入也称为**控制反转**(IoC)。这个名称来源于客户端的控制已被反转的事实,这意味着客户端不再负责其依赖项,并且控制权已移交给另一个实体。

如果您从客户端中删除依赖项,这意味着依赖项必须在另一个作用域中创建。这里使用的作用域是“其他地方”,并且这些依赖项可能自身也具有依赖项。无论如何,在依赖项(客户端)可以使用它们之前,所有依赖项都必须存在。这会带来代码膨胀和引入错误的副作用。例如,您可能知道一个类需要哪些依赖项,但您的同事可能不知道,并且可能使用不同的依赖项,这可能导致错误。这就是**依赖注入容器(DIC)**主要使用的场景。DIC是一种实现,可以在任何编程语言中实现。

依赖注入容器

DIC可以被视为一个注册表,依赖项可以在其中注册以供使用。每个依赖项都有一个名称,这可以是类名或标识依赖项的唯一名称。此外,每个依赖项都可以使用依赖项所需的参数进行配置。例如,Smtp依赖项的参数可能是hostportusernamepassword,因为它需要这些详细信息才能连接到Smtp服务器。将所有依赖项注册到容器中后,容器将负责使用get()方法返回这些依赖项的实例。这种方法将所有依赖项集中到一个容器中。您可以拥有多个容器,但最终目标是在需要时获取预先配置好的依赖项。

将已实例化的依赖项存储到DIC中存在一个主要的性能问题。考虑这种情况。您创建一个Smtp类的实例并将其对象存储到DIC中。然而,并非代码的所有部分都会使用Smtp对象,这意味着该对象已被实例化但未使用。当注册了许多依赖项但并非所有依赖项都在执行代码的上下文中被使用时,这会导致性能问题。因此,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){
        // Process arguments
    }
}

$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已注册并命名为smtpMailer类需要在其构造函数中传入一个smtp实例。在注册Mailer依赖项时,使用双**@@**后跟依赖项名称来引用先前注册的依赖项。

DIC的实现方式不同,每种编程语言或供应商都有自己的语法。一些DIC不仅限于向类构造函数传递参数,甚至可以调用带有参数的类方法。

PHP简单的反射功能使得任何人都可以轻松开发自己的DIC库。本示例项目中的代码可以轻松扩展为从配置文件注册依赖项。

这标志着本文的结束。 请随时留下您的评论和建议。

历史

  • 2014年4月12日:初始版本
© . All rights reserved.