依赖注入:几乎每个类都依赖于其他几个类

5
我正在学习PHP OOP时尝试实现最佳实践。我理解概念,但对正确的实现有所怀疑。因为我试图弄清楚基本的实现原则,所以在这段代码中我没有实现DI容器。
结构
- Db类用于数据库连接。 - Settings类,从数据库中检索设置。 - Languages类,检索特定语言的信息。 - Page类,Product类,Customer类等等。
思路 Settings类需要Db类来检索设置。 Languages类需要DbSettings来根据从数据库获取的设置检索信息。 Page类需要DbSettingsLanguages。它未来可能还需要一些其他类。
简化代码
Db.php继承PDO
Settings.php
class Settings
{
    /* Database instance */
    protected $db;

    /* Cached settings */
    private $settings   = array();

    public function __construct(Db $db)
    {
        $this->db = $db;
    }

    public function load ()
    {
        $selq = $this->db->query('SELECT setting, value FROM settings');
        $this->settings = $selq->fetchAll();
    }
}

Languages.php

class Languages
{

    public $language;

    protected $db;
    protected $settings;

    private $languages = array();

    public function __construct(Db $db, Settings $settings)
    {
        $this->db = $db;
        $this->settings = $settings;
        // set value for $this->language based on user choice or default settings
        ...
    }

    public function load() 
    {
        $this->languages = array();
        $selq = $this->db->query('SELECT * FROM languages');
        $this->languages = $selq->fetchAll();
    }

}

Page.php

class Page
{
    protected $db;
    protected $settings;
    protected $language;

    public function __construct(Db $db, Settings $settings, Languages $languages)
    {
        $this->db = $db;
        $this->settings = $settings;
        $this->languages = $languages;
    }

    public function load() 
    {
        // load page info from db with certain settings and in proper language
        ...
    }

}

Config.php

$db = new Db;

/* Load all settings */
$settings   = new Settings($db);
$settings->load();

/* Load all languages */
$languages  = new Languages($db, $settings);
$languages->load();

/* Instantiate page */
$page   = new Page($db, $settings, $languages);

我不喜欢重复地注入相同的类。这样,当我需要注入10个类时,我就会遇到问题。所以,我的代码从一开始就是错误的。

也许更好的方法是这样做:

Config.php

$db = new Db;

/* Load all settings */
$settings   = new Settings($db);
$settings->load();

/* Load all languages */
$languages  = new Languages($settings);
$languages->load();

/* Instantiate page */
$page   = new Page($languages);

由于设置(Settings)已经可以访问$db,语言(Languages)也可以同时访问$db和$settings。

然而,以这种方式,我需要进行如下调用:$this->languages->settings->db-...

我的所有代码架构似乎完全错误了 :)

应该如何做呢?

3个回答

5
我会尽力回答自己的问题,因为我研究了很多资料后得出以下结论。 1. 最佳实践是创建如下对象:
$db = new Db();

$settings = new Settings ($db);

$languages = new Languages ($db, $settings);

// ...

2. 使用依赖注入容器。

如果你不会自己写,可以使用已有的依赖注入容器。有些容器会打着依赖注入的名义,却并不是真正的依赖注入容器,比如 Pimple(这个网站上有一些相关文章)。有些容器可能更为复杂和慢(如 Zend、Symfony),但也提供了更强大的功能。如果你正在阅读本文,那么你应该选择更简单的一个,比如 Aura、Auryn、Dice、PHP-DI(按字母表顺序排列)。 同样重要的是,正确的依赖注入容器(就我所知)应该具有递归遍历依赖项的能力,也就是找到某个对象所需的依赖项。它们还应该提供共享同一对象的能力(例如 $db 实例)。

3. 手动注入依赖项会在尝试动态创建对象时造成许多问题。(如果您使用前端控制器和路由,则会出现这种情况。)因此请参考第 2 点。

查看示例:

https://github.com/rdlowrey/Auryn#user-content-recursive-dependency-instantiation

https://github.com/rdlowrey/Auryn#instance-sharing

观看视频:

https://www.youtube.com/watch?v=RlfLCWKxHJ0 (虽然不是 PHP,但尝试理解其思想)


1
有一些想法可以摆脱这些丑陋的链式依赖。考虑三个具体的类 A、B、C,其中 A 需要 B,而 B 需要 C,所以 A 隐式地需要 C。这是非常糟糕的软件设计。 测试更加复杂,因为:
  1. 如果想进行一些集成测试,则需要这三个对象
  2. 如果要隔离测试 A,则需要模拟 B 并禁用构造函数或者还必须模拟 C
在这一点上,您应该停止您的设计。要将 A 与 C 解耦,您应该创建一个接口 BI,A 依赖于它,而 B 实现它。
来自:
A -> B
B -> C

我们现在有:

A -> BI
B impl BI
B -> C

我们可以通过另一个名为 CI 的接口完全解耦它们:
A -> BI
B impl BI
B -> CI
C impl CI

现在我们的具体类已经解耦,但是如果我们像分离它们一样连接它们(例如通过 DI),那么就没有什么好处了。因此,下一步是缩小接口,例如让类A仅依赖于BI,该接口仅具有A需要的方法。
假设这样做了,我们需要一个类A2,它需要B的一些方法。您会发现实际上BI对于A2很好,因为BI具有A2需要的一些方法(我们现在将C和CI排除在外)。
A -> BI
A2 -> BI
B -> BI

几天后,您发现我们的接口BI实际上对A来说太大了,您想重构BI以使其尽可能小。但是您无法重构它,因为A2使用了您想要删除的一些方法。
这也可能是不良设计。现在我们可以做的是(接口隔离原则):
A -> BI
A2 -> BI2
B impl BI
B impl BI2

在这种情况下,我们现在可以独立地更改接口。无论您的项目使用多少类,我始终建议为应用程序的某些部分使用某种类型的创建模式(参考第一个示例),以从实现CI接口的未知实现类中获取对象。
这就是真正的面向对象编程。今天,您可能拥有一些过度设计的依赖链来获取CI,但在夜晚,您会梦想出一种开创性的想法,如何获取CI并以不需要更改使用CI的任何代码的方式进行更改!这很重要。
我总是说:隐藏实现(当然是在正确的位置)非常重要。
现在不要开始为每个类添加接口,而是让一些类之间的耦合发生,并在需要时对它们进行重构。
应用于您的类,我认为设置是一个getter/setter怪物。让只需要一些设置的类依赖于设置并不是一个好主意。我认为这样做更好:
Settings -> DB
Settings impl PageSettings
Settings impl CustomerSettings
Settings impl ProductSettings
Page -> PageSettings
Customer -> CustomerSettings
Product -> ProductSettings

我不知道你如何使用Language类,但我希望你现在已经有了设计软件的想法。当然,还有更多内容。


这正是我正在处理的问题。接口被证明是一种非常强大的工具。不过,当我提出这个问题时,我对如何使用接口一无所知。现在我面临一个更重要的问题 - 如何让 DI 容器使用这些接口,这是完全另一个问题。而且事实证明,许多现有的 DiC 处理它们的方式很奇怪(Auryn、Dice)。 - Alex Lomakin
我必须更正,A和BI将在同一个包中,因为A说它想要什么而不是“B是一个BI,A可以使用一个BI”。将Java Google Guice与PHP Symfony进行比较,Guice的方法是基于接口进行实现,因此您可以说“Guice让我得到一个CI”,并定义映射到接口的依赖树。在Symfony的DI上下文中,没有已知的接口。Symfony只使用简单的字符串作为键,例如“Symfony让我得到一个@mailer”。 - Aitch

0

如果您在一个对象内使用另一个对象的依赖项,则会在这两个组件之间创建依赖关系。这意味着,如果您需要更改您的Settings类的依赖关系,则会破坏所有依赖于您的Settings类的内容。

依赖注入库

如果您担心每次都必须手动构建新对象,请查看依赖注入库以处理您的对象创建(例如PimpleAuraPHP DI)。

使用Pimple:

// Define this once in your bootstrap
use Pimple\Container;
$container = new Container()

$container['Db'] = function ($c) {
    return new Db();
};

$container['Settings'] = function ($c) {
    return new Settings($c['Db']);
};

$container['Languages'] = function ($c) {
    return new Languages($c['Db'], $c['Settings']);
};

$container['Page'] = function ($c) {
    return new Page($c['Db'], $c['Settings'], $c['Languages']);
};


// Where ever you have access to your $container you can use this
// (and it knows how to build your object for you every time).
$page = $container['Page'];

使用访问器注入依赖项

您可以使用访问器函数来设置依赖项:

class Settings {
    protected $db;

    function setDb(Db $db) {
        $this->db = $db;
    }

    // ...
}

$settings = new Settings();
$settings->setDb(new Db());

AuraPHP DI 支持使用访问器进行依赖注入。

use Aura\Di\Container;
use Aura\Di\Factory;

$di = new Container(new Factory());
$di->set('db', new Db());
$di->set('settings', new Settings());
$di->setter['settings']['setDb'] = $di->get('db');

你甚至可以进一步扩展它,并通过基于你所扩展的接口进行注入,自动注入那些你不想在每个类上手动设置的常见依赖项(比如PSR-3 Logger)。

use Aura\Di\Container;
use Aura\Di\Factory;
use Psr\Log\LoggerAwareTrait;

class Db {
    use LoggerAwareTrait;
    // ...
}

$di = new Container(new Factory());
$di->set('logger', new MyCustomLogger());
$di->set('db', new Db());
$di->setter['LoggerAwareTrait']['setLogger'] = $di->get('logger');

// $db->logger will contain an instance of MyCustomLogger
// so will any other class that uses LoggerAwareTrait
$db = $di->get('db');

单一职责原则

当然,如果你的类有10个不同的依赖关系,那么问题可能出在设计上。一个类应该具有单一职责

可能违反单一职责原则的类的症状:

  • 类有许多实例变量
  • 类有许多公共方法
  • 类的每个方法都使用其他实例变量
  • 特定任务被委托给私有方法

来自Matthias Noback的“软件包设计原则


我的担忧不在于类的实例化,而更多地关注依赖注入的最佳实践,当多个类可能依赖于其他几个类甚至彼此之间时。我可以很容易地想象到这样一种情况,即我需要当前$language在$settings内,这意味着依赖关系将会发生变化。直接评论你的答案,你认为像new Class($x, $y, $z)这样初始化类是最佳实践吗? - Alex Lomakin
如果它们是关键组件,那么是的。如果它们是仅用于特定情况的可选组件,则访问器将是您的朋友(例如 $page->setDbHandler($db);),如果您不想一遍又一遍地复制代码,则可以使用 traits 加载它们。请参见 PSR-3 LoggerAwareTrait。当然,如果您的类有 10 种不同的依赖关系,那么问题可能出在设计上。 - Tom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接