在几乎每个人都需要访问公共数据结构的情况下,依赖项注入有什么好处?


20

在OOP中,为什么全局变量是邪恶的有很多原因。

如果需要共享的对象的数量或大小太大而无法在函数参数中有效传递,通常每个人都建议使用依赖注入而不是全局对象。

但是,在几乎每个人都需要了解某种数据结构的情况下,为什么依赖注入比全局对象更好?

示例(一个简化的示例,以大体上说明这一点,而无需在特定应用程序中深入研究)

许多虚拟车辆具有大量的属性和状态,包括类型,名称,颜色,速度,位置等。许多用户可以对其进行远程控制,并且发生大量事件(用户都可以已启动和自动)可以更改其许多状态或属性。

天真的解决方案是仅将它们制成一个全局容器,例如

vector<Vehicle> vehicles;

可以从任何地方访问。

更加面向OOP的解决方案是让容器成为处理主事件循环的类的成员,并在其构造函数中实例化。每个需要它并且是主线程成员的类都将通过其构造函数中的指针被授予对容器的访问权限。例如,如果外部消息是通过网络连接传入的,则负责解析的类(每个连接一个)将接管,解析器将可以通过指针或引用访问容器。现在,如果解析后的消息导致容器元素发生更改,或者需要容器中的某些数据来执行操作,则无需通过信号和插槽来扔掉成千上万个变量(或更糟的是,将它们存储在解析器中,稍后由调用解析器的人检索)。当然,所有通过依赖注入接收对容器的访问的类都是同一线程的一部分。不同的线程不会直接访问它,但是会执行其工作,然后将信号发送到主线程,并且主线程中的插槽将更新容器。

但是,如果大多数类都可以访问该容器,那么到底它与全局容器有什么不同呢?如果这么多的类需要容器中的数据,那么“依赖注入方式”是否只是伪装的全局变量?

一个答案将是线程安全性:即使我注意不要滥用全局容器,也许将来另一个开发人员在紧迫的最后期限的压力下,仍会在另一个线程中使用全局容器,而不会照顾所有人碰撞情况。但是,即使在依赖注入的情况下,也可能会向在另一个线程中运行的某个人提供指针,从而导致相同的问题。


6
等一下,您谈论的是非可变全局变量,并使用链接“为什么全局状态不好”来证明这一点?WTF?
Telastyn

1
我根本没有为全球人辩护。我认为很明显,我同意以下事实:全局变量不是一个好的解决方案。我只是说一种情况,即使使用依赖注入也不能比全局变量好(甚至可能与全局变量几乎没有区别)。因此,一个答案可能指出在这种情况下仍然使用依赖注入的其他潜在好处,否则其他方法将比di更好
vsz 2015年

9
但是只读全局变量和全局状态之间存在确定的区别。
Telastyn


1
@vsz不是真的-问题是关于服务定位器工厂的,它是解决许多不同对象问题的一种方法。但是它的答案也回答了您的问题,即允许类访问全局数据,而不是将全局数据传递给类。
gbjbaanb 2015年

Answers:


37

在几乎每个人都需要了解某种数据结构的情况下,为什么依赖注入比全局对象更好?

自从切片面包以来,依赖注入是最好的方法,而数十年来,众所周知的全局对象是所有邪恶的源头,因此这是一个相当有趣的问题。

依赖项注入的目的只是确保每个需要某些资源的参与者都可以拥有它,因为显然,如果将所有资源全局化,那么每个参与者都将可以访问所有资源,问题已经解决了,对吧?

依赖项注入的要点是:

  1. 允许参与者根据需要访问资源,并且
  2. 控制任何给定参与者访问资源的哪个实例

在您的特定配置中,所有参与者都需要访问同一资源实例这一事实是无关紧要的。相信我,您将有一天需要重新配置事物,以便参与者可以访问该资源的不同实例,然后您将意识到自己已经陷入困境。一些答案已经指出了这样的配置:testing

另一个示例:假设您将应用程序拆分为客户端-服务器。客户端上的所有参与者都使用客户端上的同一组中央资源,服务器上的所有参与者都使用服务器上的同一组中央资源。现在假设有一天,您决定创建客户端服务器应用程序的“独立”版本,其中客户端和服务器都打包在单个可执行文件中并在同一虚拟机中运行。(或运行时环境,取决于您选择的语言。)

如果使用依赖项注入,则可以轻松地确保为所有客户端参与者提供了可以使用的客户端资源实例,而所有服务器参与者都可以接收服务器资源实例。

如果不使用依赖项注入,那么您将完全不走运,因为每个资源只能在一个虚拟机中存在一个全局实例。

然后,您必须考虑:所有参与者真的需要访问该资源吗? 真?

您可能犯了将资源变成上帝对象的错误(因此,当然每个人都需要访问它),或者您可能严重高估了项目中实际需要访问该资源的参与者的数量。

使用全局变量,整个应用程序中的每一行源代码都可以访问每个全局资源。使用依赖注入,每个资源实例仅对实际需要它的参与者可见。如果两者相同(需要特殊资源的参与者占项目中源代码行的100%),那么您肯定在设计中犯了一个错误。所以,要么

  • 将巨大的巨大神资源重构为较小的子资源,因此不同的参与者需要访问它的不同部分,但是很少有参与者需要所有的部分,或者

  • 重构您的参与者,使其仅接受他们需要解决的问题的子集作为参数,因此他们不必一直在咨询一些巨大的大型中央资源。


我不确定我是否理解这一点-一方面您说DI允许您使用资源的多个实例,而全局则不允许,这显然是无稽之谈,除非您将单个静态变量与多个实例化对象进行了混合-没有理由“全局”必须是单个实例或单个类。
gbjbaanb

3
@gbjbaanb假设资源是Zork。OP表示,不仅系统中的每个对象都需要一个Zork才能工作,而且还说只有一个Zork会存在,并且他的所有对象都只需要访问该Zork的一个实例。我说这两个假设都不是合理的:他的所有对象都需要Zork可能并不正确,有时某些对象需要Zork的某个实例,而其他对象则需要一个Zork。 Zork的其他实例。
Mike Nakis 2015年

2
@gbjbaanb我想你不是要我解释为什么拥有两个Zork全局实例,将它们分别命名为ZorkA和ZorkB并将每个对象都将使用ZorkA以及将使用ZorkB的对象硬编码为愚蠢的做法,对?
Mike Nakis 2015年

1
我没有说所有人,我几乎所有人。问题的关键是,除了拒绝那些不需要的少数参与者之外,DI还能带来其他好处。我知道“上帝的对象”是一种反模式,但是盲目遵循最佳实践也可以是一种。程序的全部目的可能是利用某种资源来做各种事情,在这种情况下,几乎每个人都需要访问该资源。一个很好的例子是在图像上运行的程序:其中几乎所有内容都与图像数据有关。
vsz 2015年

1
@gbjbaanb我相信这个想法是让一个客户类能够按照所指出的那样专门在ZorkA或ZorkB上工作,而不是让客户类来决定要抓住哪一个。
Eugene Ryabtsev

39

非可变全局变量在OOP中是邪恶的,有很多原因。

这是一个可疑的说法。您用作证据的链接是指状态 - 可变全局变量。他们绝对是邪恶的。只读的全局变量只是常量。常量相对理智。我的意思是,您不会将pi的值注入所有类中,对吗?

如果需要共享的对象的数量或大小太大而无法在函数参数中有效传递,通常每个人都建议使用依赖注入而不是全局对象。

不,他们没有。

如果您的函数/类具有太多的依赖关系,人们建议停止并看看为什么您的设计如此糟糕。大量的依赖关系表明您的类/函数可能正在做过多的事情,并且/或者您没有足够的抽象。

许多虚拟车辆具有大量的属性和状态,包括类型,名称,颜色,速度,位置等。许多用户可以对其进行远程控制,并且发生大量事件(用户都可以已启动和自动)可以更改其许多状态或属性。

这是一场设计梦night。您有很多东西可以使用/滥用,而没有任何方式的验证,没有并发限制的方式-它只是遍地都是。

但是,如果大多数类都可以访问该容器,那么到底它与全局容器有什么不同呢?如果这么多的类需要容器中的数据,那么“依赖注入方式”是否只是伪装的全局变量?

是的,在任何地方都可以与公共数据耦合与全局没有太大区别,因此仍然很糟糕。

好处是,您正在使使用者与全局变量本身脱钩。这为您提供创建实例的灵活性。它也不会强迫您只有一个实例。您可以将不同的产品传递给不同的消费者。然后,您还可以根据需要为每个使用者创建不同的接口实现。

由于变化不可避免地伴随着软件需求的变化,因此这种基本的灵活性至关重要


对于“ non-mutable”的困惑,我们深表歉意,我想说一句易变的,但不知何故没有认出我的错误。
vsz 2015年

“这是一场设计梦m”-我知道。但是,如果要求所有这些事件必须能够执行数据中的更改,那么即使我将它们拆分并隐藏在抽象层下,这些事件也将与数据耦合。整个程序就是关于此数据的。例如,如果程序必须在图像上执行许多不同的操作,则所有或几乎所有类都将以某种方式耦合到图像数据。仅仅说“让我们编写一个执行不同功能的程序”是不可接受的。
vsz 2015年

2
具有许多可以定期访问某些数据库的对象的应用程序并不是完全前所未有的。
罗伯特·哈维

@RobertHarvey-可以,但是它们所依赖的接口是“可以访问数据库”还是“某些持久性数据存储库foo”?
Telastyn

1
让我想起了Dilbert,PHB要求提供500个功能,Dilbert则拒绝。因此,PHB要求提供1个功能,而Dilbert表示肯定。第二天,Dilbert收到1个功能的500个请求。如果某些东西无法缩放,那么无论您如何打扮,它都不会缩放。
corsiKa

16

您应考虑三个主要原因。

  1. 可读性。如果每个代码单元都具有注入或作为参数传递所需的所有工作,则可以轻松查看代码并立即查看其工作情况。这使您可以局部使用功能,还可以使您更好地分离关注点,并迫使您考虑...
  2. 模块化。解析器为什么要知道整个车辆清单及其所有属性?可能没有。也许它需要查询车辆ID是否存在,或者车辆X是否具有属性Y。突然之间,您最终得到了一个有意义得多的设计,每一位代码仅处理与之相关的数据。这导致我们...
  3. 可测试性。对于典型的单元测试,您想要针对许多不同的场景设置环境,而注入使这种实现非常容易。再一次,返回解析器示例,您是否真的想始终为您为解析器编写的每个测试用例手工制作完整的完整车辆清单?还是只创建上述服务对象的模拟实现,返回不同数量的ID?我知道我会选择哪一个。

综上所述:DI不是目标,它只是使实现目标成为可能的工具。如果您滥用它(就像您在示例中所做的那样),您将无法更接近目标,并且DI的神奇特性不会使不良设计变得更好。


+1可获得针对维护问题的简洁答案。更多的P:SE答案应该像这样。
dodgethesteamroller 2015年

1
你的总结太好了。太好了。如果您认为[本月的设计模式]或[本月的流行语]可以神奇地解决您的问题,那么您将会遇到麻烦。
corsiKa 2015年

@corsiKa我很长时间以来都不喜欢DI(或者更确切地说是Spring),因为我看不出它的意义。看起来似乎很麻烦,却没有明显的收获(这是在XML配置时代开始的)。我希望我能找到一些关于DI当时能买到什么的文件。但是我没有,所以我必须自己弄清楚。花了很长时间。:)
biziclop

3

它确实涉及可测试性-通常,全局对象非常易于维护且易于读取/理解(当然,一旦您知道那里就可以了),但这是一个永久性的工具,当您将类分为独立的测试时,您会发现自己的全局对象卡住了,说“关于我的事”,因此孤立的测试要求将全局对象包含在其中。大多数测试框架不能轻松支持这种情况并没有帮助。

是的,它仍然是全球性的。拥有它的根本原因没有改变,只是改变了它的访问方式。

至于初始化,这仍然是一个问题-您仍然必须设置配置以便可以运行测试,因此您无法获得任何好处。我想您可以将一个较大的依赖对象拆分为许多较小的对象,然后将适当的对象传递给需要它们的类,但是您可能会变得更加复杂,以至于无法获得任何好处。

无论如何,它都是“尾巴摇狗”的例子,测试的需求正在驱动代码库的架构(类似静态类的项目,例如当前日期时间,也会出现类似的问题)。

就我个人而言,我更喜欢将我的所有全局数据粘贴到一个(或多个)全局对象中,但提供访问器来获取类需要的位。然后,我可以模拟那些数据以返回测试所需的数据,而无需增加DI的复杂性。


“通常,全局对象是非常可维护的”-如果它是可变的,则不是。以我的经验,拥有如此多可变的全球状态可能是一场噩梦(尽管有很多方法可以使它成为可以忍受的)。
sleske

3

除了您已经拥有的答案之外,

许多虚拟车辆具有大量的属性和状态,包括类型,名称,颜色,速度,位置等。许多用户可以对其进行远程控制,并且发生大量事件(用户都可以已启动和自动)可以更改其许多状态或属性。

OOP编程的特征之一是仅保留特定对象真正需要的属性,

现在,如果您的对象具有太多的属性,则应将对象进一步细分为子对象。

编辑

已经提到了为什么全局对象不好,我将在其中添加一个示例。

您需要在Windows窗体的GUI代码上进行单元测试,如果使用全局,则需要创建所有按钮及其事件,例如,以创建适当的测试用例...


是的,但是例如,由于可能会收到任何可以对任何内容进行更改的命令,因此解析器将必须了解所有内容。
vsz 2015年

1
“在我提出您的实际问题之前” ...您要回答他的问题吗?
gbjbaanb

@gbjbaanb问题有很多文字,请给我一些时间正确阅读
Mathematics
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.