十三、解决依赖关系

编写松散耦合的代码已成为任何专业开发人员的一项基本技能。虽然传统应用程序倾向于将其全部打包,从而最终形成一大块坚实的代码,但现代应用程序采用了更为渐变的方法,因为它们在很大程度上依赖于第三方库和其他组件。如今,几乎没有人构建自己的邮件程序、记录器、路由器、模板引擎等等。这些组件中有大量存在,等待我们的应用程序通过 Composer 使用。由于各个组件本身由各种社区或商业实体进行测试和维护,因此维护应用程序的成本大大降低。更专业的开发人员处理可能不属于我们专业领域的特定功能,从而间接提高了总体代码质量。通过松散耦合的代码实现的和谐。

松散耦合代码有许多积极的副作用,其中一些包括:

  • 更容易的重构
  • 改进的代码可维护性
  • 更容易跨平台利用
  • 更容易跨框架利用
  • 对单一责任原则合规的渴望
  • 更容易的测试

通过利用各种语言功能(如接口)和设计模式(如依赖项注入),可以轻松实现这种松耦合的魔力。接下来,我们将通过以下部分了解依赖项注入的基本方面:

  • 缓解共同问题
  • 理解依赖注入
  • 理解依赖注入容器

缓解共同问题

依赖注入是一种成熟的软件技术,它处理对象依赖性问题,允许我们编写松散耦合的类。虽然模式本身已经存在了相当长的一段时间,但 PHP 生态系统直到 Symfony 等主要框架开始实现它之后才真正开始使用它。如今,它已成为除琐碎应用程序之外的任何其他应用程序的事实标准。通过一个简单的例子可以很容易地观察到整个依赖性问题:

<?php

class Customer
{
    protected $name;

    public function loadByEmail($email)
    {
        $mysqli = new mysqli('127.0.0.1', 'foggy', 'h4P9niq5', 'sakila');

        $statement = $mysqli->prepare('SELECT * FROM customer WHERE email = ?');
        $statement->bind_param('s', $email);
        $statement->execute();

        $customer = $statement->get_result()->fetch_object();

        $this->name = $customer->first_name . ' ' . $customer->last_name;

        return $this;
    }
}

$customer = new Customer();
$customer->loadByEmail('MARY.SMITH@sakilacustomer.org');

在这里,我们有一个简单的Customer类和一个loadByEmail()方法。令人不安的是对数据库$mysqli对象的依赖被锁定在loadByEmail()实例方法中。这有利于紧密耦合,从而降低代码的可重用性,并为以后的代码更改可能导致的系统范围的副作用打开大门。为了缓解此问题,我们需要将数据库$mysqli对象注入$customer

The MySQL Sakila database can be obtained from https://dev.mysql.com/doc/sakila/en/.

有三种方法可以将依赖项注入对象

  • 通过实例方法
  • 通过类构造函数
  • 通过实例属性

而实例方法和类构造函数方法似乎比实例属性注入更受欢迎。

下面的示例演示了使用实例方法进行依赖项注入的方法:

<?php

class Customer
{
    public function loadByEmail($email, $mysqli)
    {
        // ...
    }
}

$mysqli = new mysqli('127.0.0.1', 'foggy', 'h4P9niq5', 'sakila');

$customer = new Customer();
$customer->loadByEmail('MARY.SMITH@sakilacustomer.org', $mysqli);

这里,我们通过客户的loadByEmail()实例方法将$mysqli对象的实例注入Customer对象的实例中。虽然这肯定比在loadByEmail()方法中实例化$mysqli对象要好,但很容易想象,如果我们的类有十几个方法,每个方法都需要向其传递不同的对象,那么我们的客户机代码会变得多么笨拙。虽然这种方法看起来很诱人,但通过实例方法注入依赖项违反了 OOP 的封装原则。此外,为了依赖性而向方法添加参数绝非最佳实践的一个例子。

另一种方法是根据以下示例使用类构造函数方法:

<?php

class Customer
{
    public function __construct($mysqli)
    {
        // ...
    }

    public function loadByEmail($email)
    {
        // ...
    }
}

$mysqli = new mysqli('127.0.0.1', 'foggy', 'h4P9niq5', 'sakila');

$customer = new Customer($mysqli);
$customer->loadByEmail('MARY.SMITH@sakilacustomer.org');

这里,我们通过客户的__constructor()方法将$mysqli对象的实例注入Customer对象的实例中。无论是注入单个对象还是十几个对象,构造函数注入在这里都是明显的赢家。客户机应用程序对所有注入都有一个单一的入口点,这使得跟踪事情变得很容易。

如果没有依赖注入的概念,就不可能实现松散耦合的代码。

理解依赖注入

在整个介绍部分中,我们讨论了通过类__construct()方法传递依赖关系。这不仅仅是传递依赖对象。让我们考虑下面三个看似相似但不同的例子。

尽管 PHP 支持类型暗示已经有相当长的一段时间了,但遇到以下代码片段并不少见:

<?php

class App
{
    protected $config;
    protected $logger;

    public function __construct($config, $logger)
    {
        $this->config = $config;
        $this->logger = $logger;
    }

    public function run()
    {
        $this->config->setValue('executed_at', time());
        $this->logger->log('executed');
    }
}

class Config
{
    protected $config = [];

    public function setValue($path, $value)
    {
        // implementation
    }
}

class Logger
{
    public function log($message)
    {
        // implementation
    }
}

$config = new Config();
$logger = new Logger();

$app = new App($config, $logger);
$app->run();

我们可以看到,App__construct()方法没有利用 PHP 类型暗示特性。开发人员假设$config$logger变量属于某种类型。虽然这个例子可以很好地工作,但它仍然使我们的类紧密耦合。这个例子和上一个例子没有太大区别,我们在loadByEmail()方法中有$msqli依赖关系。

向混合中添加类型暗示允许我们强制传递到App__construct()方法中的类型:

<?php

class App
{
    protected $config;
    protected $logger;

    public function __construct(Config $config, Logger $logger)
    {
        $this->config = $config;
        $this->logger = $logger;
    }

    public function run()
    {
        $this->config->setValue('executed_at', time());
        $this->logger->log('executed');
    }
}

class Config
{
    protected $config = [];

    public function setValue($path, $value)
    {
        // implementation
    }
}

class Logger
{
    public function log($message)
    {
        // implementation
    }
}

$config = new Config();
$logger = new Logger();

$app = new App($config, $logger);
$app->run();

这一简单的步骤使我们的代码松散耦合到了一半。尽管我们现在指示我们的可注入对象是一个精确的类型,但我们仍然锁定在一个特定的类型上,即实现;否则,依赖注入模式就不会有太多的用途。

第三个示例对前两个示例进行了重要区分:

<?php

class App
{
    protected $config;
    protected $logger;

    public function __construct(ConfigInterface $config, LoggerInterface $logger)
    {
        $this->config = $config;
        $this->logger = $logger;
    }

    public function run()
    {
        $this->config->setValue('executed_at', time());
        $this->logger->log('executed');
    }
}

interface ConfigInterface
{
    public function getValue($value);

    public function setValue($path, $value);
}

interface LoggerInterface
{
    public function log($message);
}

class Config implements ConfigInterface
{
    protected $config = [];

    public function getValue($value)
    {
        // implementation
    }

    public function setValue($path, $value)
    {
        // implementation
    }
}

class Logger implements LoggerInterface
{
    public function log($message)
    {
        // implementation
    }
}

$config = new Config();
$logger = new Logger();

$app = new App($config, $logger);
$app->run();

支持接口类型提示而不是具体的类类型提示是编写松散耦合代码的关键因素之一。尽管我们仍在通过类__construct()注入依赖项,但我们现在是以程序的方式向接口注入依赖项,而不是以实现的方式。这使我们能够避免紧耦合,使我们的代码更加可重用。

显然,这些例子最终都很简单。我们可以想象,当注入对象的数量增加时,事情会变得多么复杂,每个注入对象本身可能需要一个、两个甚至十几个__construct()参数。这就是依赖项注入容器的用武之地。

理解依赖注入容器

依赖项注入容器是一个知道如何将类自动关联在一起的对象。自动连线术语意味着实例化和正确配置对象。这绝不是一项容易的任务,这就是为什么有几个库处理此功能的原因。

Symfony 框架提供的 DependencyInjection 组件是一个整洁的依赖项注入容器,可以由 Composer 轻松安装。 继续,让我们创建一个di-container目录,我们将在其中执行这些命令并设置我们的项目:

composer require symfony/dependency-injection

结果表明,我们应该安装一些额外的软件包:

我们需要确保通过运行以下控制台命令来添加symfony/yamlsymfony/config包:

composer require symfony/yaml
composer require symfony/config

symfony/yaml包安装 Symfony Yaml 组件。该组件将 YAML 字符串解析为 PHP 数组,反之亦然。symfony/config包安装 Symfony 配置组件。该组件提供类来帮助我们从源(如 YAML、XML、INI 文件,甚至数据库本身)中查找、加载、组合、自动填充和验证配置值。symfony/dependency-injectionsymfony/yamlsymfony/config包本身就是松耦合组件的一个很好的例子。虽然这三个包齐头并进地提供了完整的依赖注入功能,但组件本身遵循松耦合原则。

Check out http://symfony.com/doc/current/components/dependency_injection.html for more information about the Symfony's DependencyInjection component.

现在让我们继续在di-container目录中创建container.yml配置文件:

services:
  config:
    class: Config
 logger:
    class: Logger
 app:
    class: App
 autowire: true

container.yml文件具有以关键字services开头的特定结构。不必深入研究,只需说服务容器是依赖注入容器的 Symfony 名称,而服务是执行某些任务的任何 PHP 对象——基本上是任何种类的类的实例。

services标签的正下方,我们有configloggerapp标签。这表示三种不同服务的声明。我们可以很容易地将它们命名为the_configthe_loggerthe_app或其他我们喜欢的名称。深入到单个服务中,我们看到class标签在所有三个服务中都是通用的。class标记告诉容器在请求给定服务实例时要实例化哪个类。最后,app服务定义中使用的autowire特性允许自动连线子系统通过解析App类的构造函数来检测其依赖性。这使得客户端代码在不知道App__construct()$config$logger要求的情况下获取App类的实例变得非常简单。

有了container.yml文件,我们继续在di-container目录中创建index.php文件:

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

interface ConfigInterface { /* ... */}
interface LoggerInterface { /* ... */}
class Config implements ConfigInterface { /* ... */}
class Logger implements LoggerInterface { /* ... */}
class App { /* ... */}

// Bootstrapping
$container = new ContainerBuilder();

$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('container.yml');

$container->compile();

// Client code
$app = $container->get('app');
$app->run();

Be sure to replace everything from ConfigInterface to App with the exact code we had in our third example from within the Understanding dependency injection section.

我们从包含autoload.php文件开始,这样我们的依赖容器组件就可以自动加载了。use语句后面的代码与我们在理解依赖项注入部分中的代码相同。接下来是有趣的部分。创建ContainerBuilder实例并传递到YamlFileLoader上,由YamlFileLoader加载container.yml文件,加载完文件后,我们在$container实例上调用compile()方法。运行compile()可以让容器在autowire服务标签上拾取其他内容。最后,我们在$container实例上使用get()方法获取app服务的一个实例。在这种情况下,客户机对传递给App实例的参数没有预先了解;依赖项容器根据container.yml配置自行处理。

使用接口类型提示和容器,我们能够编写更多可重用、可测试和解耦的代码。

Check out http://symfony.com/doc/current/service_container.html for more information about the Symfony service container.

总结

依赖注入是一种简单的技术,它允许我们摆脱紧耦合的束缚。结合接口类型提示,我们获得了一种编写松散耦合代码的强大技术。这将隔离并最小化未来可能的应用程序设计更改及其缺陷的影响。如今,即使编写模块化和大型代码库应用程序而不采用这些简单技术,也被认为是不负责任的。

接下来,我们将更深入地了解围绕 PHP 包的生态系统状态、它们的创建和分发。