哪个更好:依赖注入+注册表还是仅使用依赖注入或全局注册表?

16
首先,我希望将此问题限制在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 和全局变量时经常提到的一件事是,全局变量会阻碍您正确测试程序的能力。全局变量究竟如何会防止我进行 DI 不会的测试呢?我已经在很多地方读到过,这是因为全局变量可以从任何地方更改,因此难以模拟。然而,对于我来说,由于至少在 PHP 中,对象是按引用传递的,所以更改某个类中注入的对象也将更改它注入到的任何其他类中的对象。


好的,有一个小细节。在PHP 5中(我认为是5.2,但可能更早),当对象传递给方法/函数时,所有对象都通过引用传递。因此,您实际上并没有移动大量数据,而只是增加了引用计数并移动了指针...(这是您稍后在帖子中暗示的内容)... - ircmaxell
这是否意味着在传递一个没有属性和一个有1000个属性的对象之间,在性能方面不应该有任何区别(当然,除了它们初始化时的原始差异)?递归会以任何方式改变这种情况吗? - Rupert Madden-Abbott
递归是一个问题,例如,除非销毁对象的所有引用,否则无法随意销毁对象。在某些__destructor中放置debug_backtrace()通常会导致脚本崩溃,这仅仅是因为递归。提到递归是很好的,应该认真考虑。我认为注入使递归变得更糟。至于数据库连接,全局函数get_db_conn()可以很好地完成工作,你只需要在get_db_conn()函数中放置一个静态$database_conn=null;即可。 - Melsi
2个回答

13
让我们逐个解决这个问题。
首先,注册表类将包含大量的递归。
您不必将注册表类注入到数据库类中。您可以在注册表上拥有专用的方法来创建所需的类。或者,如果您注入了注册表,您可以只获取所需的内容,而不是将其存储起来,以便正确实例化类。没有递归。
请注意,全局注册表不会受到此问题的影响,因为它不会传递给任何东西。
注册表本身可能没有递归,但是注册表中的对象很可能存在循环引用。这可能导致在使用PHP 5.3之前的版本中从注册表中取消设置对象时出现内存泄漏,因为垃圾回收器无法正确收集这些对象。
其次,我将开始向不需要大量数据的对象传递数据。我的数据库并不关心我的路由器,但是路由器将与数据库连接详细信息一起传递给数据库。
没错。但这就是注册表的作用。它与将$_GLOBALS传递给对象并没有太大区别。如果你不想这样做,就不要使用注册表,而是只传递类实例所需的参数,以使其处于有效状态。或者干脆不要存储它。
我可以这样做 $this->registry->router->registry->database 很少有路由器会公开获取注册表的方法。你无法通过router从$this访问database,但你可以直接访问database。当然可以。这就是注册表的作用。这就是你为其编写的目的。如果你想将注册表存储在对象中,你可以将其包装成一个分离接口,只允许访问其中一部分数据。
显然,当我在没有注册表的情况下使用DI时,这些问题就不存在了,但是这样我就必须手动传递每个对象,导致类构造函数参数数量荒谬多多。
不一定。当使用构造函数注入时,您可以将参数数量限制为仅绝对必要以使对象处于有效状态的参数。其余的可选依赖项也可以通过setter注入来设置。此外,没有人阻止您将参数添加到数组或配置对象中。或者使用Builders
考虑到这两种DI版本的问题,使用全局注册表是否更好?通过使用全局注册表,您将此依赖性与类紧密耦合。这意味着使用这些类时,不能再不使用这个具体的注册表类。您假设只会有这个注册表,而不是其他实现。当注入依赖项时,您可以自由地注入任何满足依赖项责任的内容。
讨论DI与全局变量时经常提到的一件事是,全局变量会阻碍你正确测试程序的能力。那么全局变量到底如何阻止我测试一个不使用DI的程序呢?
它们并不会阻止你测试代码。它们只是让测试变得更困难。在单元测试中,你希望系统处于已知且可重现的状态。如果你的代码依赖于全局状态,你必须在每次测试运行时创建这个状态。
我在很多地方读到,这是因为全局变量可以从任何地方修改,因此很难进行模拟。
如果一个测试改变了全局状态,那么如果你不将其改回来,它可能会影响到后续的测试。这意味着你需要花费一些精力来重新创建环境,除了将你的被测对象设置为已知状态。如果只有一个依赖项,这可能很容易,但如果有很多依赖项,并且它们也依赖于全局状态,那么你将陷入“依赖地狱”。

5

我会将这篇文章作为答案发布,因为我想包含代码。

我已经对传递对象和使用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版本上进行测试


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