哪个更好:依赖注入+注册表或依赖注入或全局注册表?

首先,我想将这个问题仅限于Web开发。因此,只要该语言用于Web开发,这就是与语言无关的。就个人而言,我是从PHP的背景来的。

通常,我们需要使用来自多个作用域的对象。例如,我们可能需要在正常范围内使用数据库类,但也需要在控制器类中使用数据库类。如果我们在正常范围内创建数据库对象,则无法从控制器类内部访问它。我们希望避免在不同的作用域中创建两个数据库对象,因此需要一种重用数据库类的方法,而不管作用域如何。为此,我们有两种选择:

  1. 使数据库对象成为全局对象,以便可以从任何位置访问它。
  2. 例如,将数据库类以参数的形式传递给控制器的构造函数。这称为依赖关系注入 (DI)。

当有许多类涉及许多不同作用域中所有要求苛刻的对象时,问题变得更加复杂。在这两种解决方案中,这都会成为问题,因为如果我们使每个对象都是全局的,那么就会将太多的噪声放入全局范围,并且如果我们向类传递太多参数,则该类将变得更加难以管理。

因此,在这两种情况下,您经常会看到注册表的使用。在全局情况下,我们有一个注册表对象,该对象被设置为全局,然后将所有对象和变量添加到该对象中,使它们在任何对象中都可用,但只将单个变量(注册表)放入全局范围。在 DI 情况下,我们将注册表对象传递到每个类中,将参数数减少到 1。

就个人而言,我使用后一种方法,因为许多文章主张它而不是使用全局变量,但我遇到了两个问题。首先,注册表类将包含大量的递归。例如,注册表类将包含数据库类所需的数据库登录变量。因此,我们需要将注册表类注入到数据库中。但是,许多其他类将需要该数据库,因此需要将数据库添加到注册表中,从而创建一个循环。现代语言可以很好地处理这个问题,还是会导致巨大的性能问题?请注意,全局注册表不会受到此影响,因为它不会传递到任何内容中。

其次,我将开始将大量数据传递给不需要它的对象。我的数据库不关心我的路由器,但路由器将与数据库连接详细信息一起传递到数据库。通过递归问题使情况变得更糟,因为如果路由器具有注册表,注册表具有数据库和注册表,并且注册表被传递到数据库,则数据库通过路由器传递给自身(即,我可以从数据库类内部执行”)。$this->registry->router->registry->database

此外,除了更复杂的之外,我没有看到DI给我带来了什么。我必须将一个额外的变量传递到每个对象中,并且我必须使用注册表对象而不是.现在,这显然不是一个大问题,但如果它没有给我任何关于全球方法的东西,它似乎确实是不必要的。$this->registry->object->method()$registry->object->method()

显然,当我在没有注册表的情况下使用DI时,这些问题并不存在,但是我必须“手动”传递每个对象,从而导致类构造函数具有荒谬的参数数量。

考虑到两个版本的 DI 都存在这些问题,全局注册表不是更优越吗?通过对 DI 使用全局注册表会丢失什么?

在讨论DI与Globals时经常提到的一件事是,全局变量会抑制您正确测试程序的能力。全局变量究竟如何阻止我测试 DI 无法测试的程序?我在很多地方读到过,这是因为全局可以从任何地方改变,因此很难被嘲笑。但是,在我看来,由于至少在PHP中,对象是通过引用传递的,因此在某个类中更改注入的对象也会在注入它的任何其他类中更改它。


答案 1

让我们一个接一个地解决这个问题。

首先,注册表类将包含大量的递归

您不必将注册表类注入到数据库类中。您也可以在注册表上使用专用方法来为您创建所需的类。或者,如果注入注册表,则可以简单地不存储它,而只能从中获取正确实例化类所需的内容。无递归。

请注意,全局注册表不会受到此影响,因为它不会传递到任何内容中。

注册表本身可能没有递归,但注册表中的对象很可能具有循环引用。当使用 PHP 版本 5.3 之前的注册表取消设置对象时,这可能会导致内存泄漏,此时垃圾回收器无法正确收集这些对象。

其次,我将开始将大量数据传递给不需要它的对象。我的数据库不关心我的路由器,但路由器将与数据库连接详细信息一起传递到数据库。

真。但这就是注册表的用途。这与将_GLOBALS美元传递到您的对象中没有太大区别。如果您不希望这样做,请不要使用注册表,而只传入类实例处于有效状态所需的参数。或者干脆不存储它。

我可以做$this->注册表->路由器->注册表>数据库

路由器不太可能公开公共方法来获取注册表。您将无法通过 到达,但您将能够直接到达。当然。这是一个注册表。这就是你写它的目的。如果要将注册表存储在对象中,可以将它们包装到一个隔离接口中,该接口仅允许访问其中包含的数据子集。database$thisrouterdatabase

显然,当我在没有注册表的情况下使用DI时,这些问题并不存在,但是我必须“手动”传递每个对象,从而导致类构造函数具有荒谬的参数数量。

不一定。使用构造函数注入时,可以将参数数限制为将对象置于有效状态绝对必要的参数数。其余的可选依赖关系也可以通过 setter 注入进行设置。此外,没有人会阻止您在数组或配置对象中添加参数。或者使用生成器

考虑到两个版本的 DI 都存在这些问题,全局注册表不是更优越吗?通过对 DI 使用全局注册表会丢失什么?

使用全局注册表时,将此依赖项紧密耦合到类。这意味着如果没有这个具体的注册表类,就无法再使用 using 类。您假设只有此注册表,而不是不同的实现。在注入依赖关系时,您可以自由地注入任何履行依赖关系责任的东西。

在讨论DI与Globals时经常提到的一件事是,全局变量会抑制您正确测试程序的能力。全局变量究竟如何阻止我测试 DI 无法测试的程序?

它们不会阻止您测试代码。他们只是让它变得更难。当单元测试时,您希望系统处于已知且可重现的状态。如果代码依赖于全局状态,则必须在每次测试运行时创建此状态。

我在很多地方读到过,这是因为全局可以从任何地方改变,因此很难被嘲笑。

正确,如果一个测试更改了全局状态,则如果不将其更改回去,则可能会影响下一个测试。这意味着除了将“被测试主题”设置为已知状态外,还必须努力重新创建环境。如果只有一个依赖项,这可能很容易,但是如果有很多依赖项并且这些依赖项也依赖于全局状态,该怎么办?你最终会进入依赖地狱


答案 2

我会把这个作为答案发布,因为我想包含代码。

我已经对传递对象与使用进行了基准测试。我基本上创建了一个相对简单的对象,但是一个具有自我引用和嵌套对象的对象。global

结果:

Passed Completed in 0.19198203086853 Seconds
Globaled Completed in 0.20970106124878 Seconds

如果我删除嵌套对象和自引用,结果是相同的...

所以,是的,这两种不同的数据传递方法之间似乎没有真正的性能差异。因此,做出更好的架构选择(恕我直言,这就是依赖注入)...

脚本:

$its = 10000;
$bar = new stdclass();
$bar->foo = 'bar';
$bar->bar = $bar;
$bar->baz = new StdClass();
$bar->baz->ar = 'bart';

$s = microtime(true);
for ($i=0;$i<$its;$i++) passed($bar);
$e = microtime(true);
echo "Passed Completed in ".($e - $s) ." Seconds\n";

$s = microtime(true);
for ($i=0;$i<$its;$i++) globaled();
$e = microtime(true);
echo "Globaled Completed in ".($e - $s) ." Seconds\n";

function passed($bar) {
    is_object($bar);
}

function globaled() {
    global $bar;
    is_object($bar);
}

在 5.3.2 上测试


推荐