使用依赖注入的缺点是什么?

我试图在工作中引入DI模式,我们的一位主要开发人员想知道:使用依赖注入模式的缺点是什么(如果有的话)?

注意,如果可能的话,我在这里寻找一个详尽的列表,而不是关于这个主题的主观讨论。


澄清:我说的是依赖注入模式(参见Martin Fowler的这篇文章), 是一个特定的框架,无论是基于xml的(如Spring)还是基于代码的(如Guice),还是“自滚动”。


编辑:这里正在进行一些伟大的进一步讨论/咆哮/辩论。

93034 次浏览

这是我自己的第一反应:基本上任何模式都有同样的缺点。

  • 学习是需要时间的
  • 如果误解了,就会弊大于利
  • 如果走到极端,工作量可能会超过收益

代码可读性。由于依赖关系隐藏在XML文件中,因此您无法轻松地找出代码流。

以下几点:

  • DI增加了复杂性,通常是通过增加类的数量,因为责任分离得更多,这并不总是有益的
  • 您的代码将(在某种程度上)耦合到您使用的依赖注入框架(或者更一般地说,如何决定实现DI模式)
  • 执行类型解析的DI容器或方法通常会导致轻微的运行时损失(非常可以忽略不计,但它确实存在)

通常,解耦的好处是使每个任务更易于阅读和理解,但增加了编排更复杂任务的复杂性。

如果您有一个自己开发的解决方案,依赖项就会在构造函数中直接出现。或者作为方法参数,这也不难发现。尽管框架管理的依赖关系,如果走到极端,就会开始变得像魔术一样。

然而,在太多的类中有太多的依赖项是一个明显的迹象,表明你的类结构搞砸了。因此,在某种程度上,依赖注入(自行开发或框架管理)可以帮助发现那些可能隐藏在暗处的突出设计问题。


为了更好地说明第二点,这里是这个文章 (原始来源)的摘录,我全心全意地相信这是构建任何系统的基本问题,而不仅仅是计算机系统。

假设你想要设计一个大学校园。你必须将部分设计委托给学生和教授,否则物理大楼就不适合物理人员。没有一个架构师知道足够多的物理知识让人们自己去做。但你不能把每个房间的设计都委托给它的居住者,因为那样你会得到一大堆瓦砾。

你如何在一个大的层次结构中分配设计责任,同时保持整体设计的一致性和和谐?这是Alexander试图解决的架构设计问题,但它也是计算机系统开发的一个基本问题。

DI能解决这个问题吗?没有。但它确实帮助你清楚地看到,如果你试图把设计每个房间的责任委托给它的居住者。

它可以增加应用启动时间,因为IoC容器应该以适当的方式解析依赖关系,有时需要进行多次迭代。

如果你使用的是没有IOC容器的DI,最大的缺点是你很快就能看到你的代码有多少依赖关系,以及所有东西的耦合有多紧密。(“但我认为这是一个很好的设计!”)自然的进展是转向IOC容器,它可能需要一点时间来学习和实现(不像WPF学习曲线那么糟糕,但它也不是免费的)。最后一个缺点是,一些开发人员将开始编写诚实的单元测试,这将花费他们时间来弄清楚。以前只需要半天就能搞定的开发人员,现在却要花两天时间来模拟所有依赖项。

与Mark Seemann的回答类似,最重要的是你要花时间成为一个更好的开发人员,而不是把代码拼凑在一起,然后把它扔出去/投入生产。你的企业更愿意选择哪一种?只有你能回答这个问题。

控制反转(不是完全依赖注入,但已经足够接近了)最大的“缺点”是,它倾向于去掉一个点来查看一个算法的概述。这基本上就是当你有解耦的代码时所发生的事情——在一个地方查看的能力是紧密耦合的产物。

我发现构造函数注入会导致构造函数又大又丑(我在整个代码库中都使用它——也许我的对象太细了?)此外,有时使用构造函数注入会导致可怕的循环依赖(尽管这种情况非常罕见),因此您可能会发现自己必须在更复杂的系统中拥有某种就绪状态生命周期,并进行几轮依赖注入。

然而,我更喜欢构造器注入而不是setter注入,因为一旦我的对象被构造,那么我就可以毫无疑问地知道它处于什么状态,它是在单元测试环境中还是加载到某个IOC容器中。这,以一种迂回的方式说,我觉得是setter注入的主要缺点。

(作为旁注,我确实发现整个主题相当“宗教”,但你的成就将随着开发团队的技术狂热程度而变化!)

依赖注入是一种技术或模式,与任何框架无关。您可以手动连接依赖项。DI帮助您实现SR(单一职责)和SoC(关注点分离)。DI会带来更好的设计。从我的观点和经验没有缺点。就像任何其他模式一样,你可能会弄错或误用它(但在DI的情况下很难)。

如果您使用框架将DI作为原则引入到遗留应用程序中,那么您可能犯的最大错误就是将其误用为服务定位器。DI+框架本身是伟大的,只是让事情变得更好,我看到它!从组织的角度来看,每一个新的过程、技术、模式……都有共同的问题:

  • 你必须训练你的团队
  • 您必须更改您的应用程序(这包括风险)

一般来说,你必须投入时间和金钱,除此之外,没有缺点,真的!

同样的基本问题,你经常遇到的面向对象编程,样式规则和其他一切。这是可能的——事实上是非常常见的——做太多的抽象,添加太多的间接,并且通常在错误的地方过度地应用好的技术。

您应用的每个模式或其他构造都会带来复杂性。抽象和间接分散了信息,有时会移除了无关的细节,但有时也会让人更难理解到底发生了什么。你应用的每一条规则都会带来不灵活性,排除了可能是最佳方法的选择。

重点是编写能够完成这项工作的代码,并且是健壮的、可读的和可维护的。你是软件开发人员,而不是象牙塔建造者。

相关的链接

http://thedailywtf.com/Articles/The_Inner-Platform_Effect.aspx

http://www.joelonsoftware.com/articles/fog0000000018.html


可能依赖注入最简单的形式(别笑)是一个参数。依赖代码依赖于数据,而数据是通过传递参数的方式注入的。

是的,这很愚蠢,而且它没有解决依赖注入的面向对象问题,但是函数式程序员会告诉你(如果你有第一类函数)这是你唯一需要的依赖注入。这里的重点是举一个简单的例子,并展示潜在的问题。

让我们以这个简单的传统函数为例——c++语法在这里并不重要,但我必须以某种方式拼写它……

void Say_Hello_World ()
{
std::cout << "Hello World" << std::endl;
}

我有一个依赖,我想提取出来并注入-文本“Hello World”。很容易…

void Say_Something (const char *p_text)
{
std::cout << p_text << std::endl;
}

为什么它比原来的更不灵活呢?如果我决定输出应该是unicode。我可能想从std::cout切换到std::wcout。但这意味着我的字符串必须是wchar_t类型,而不是char类型。要么必须更改每个调用方,要么(更合理地)将旧的实现替换为转换字符串并调用新实现的适配器。

这是维修工作如果我们保留原来的就不需要了。

如果它看起来微不足道,那么看看这个来自Win32 API的真实函数……

http://msdn.microsoft.com/en-us/library/ms632680%28v=vs.85%29.aspx

这有12个“依赖项”需要处理。例如,如果屏幕分辨率变得非常大,也许我们将需要64位的坐标值-和另一个版本的CreateWindowEx。是的,已经有一个旧版本仍然存在,它可能会在幕后映射到新版本……

http://msdn.microsoft.com/en-us/library/ms632679%28v=vs.85%29.aspx

这些“依赖关系”不仅仅是原始开发人员的问题——每个使用该接口的人都必须查找依赖关系是什么,它们是如何指定的,以及它们的含义,并确定为他们的应用程序做什么。这就是“明智的默认”可以让生活简单得多的地方。

面向对象的依赖注入在原理上没有什么不同。无论是在源代码文本中还是在开发人员时间中,编写类都是一种开销,如果编写该类是为了根据某些依赖对象规范提供依赖项,那么依赖对象将被锁定为支持该接口,即使需要替换该对象的实现。

这些都不应该被解读为依赖注入是不好的——远非如此。但是任何好的技术都可能被过度地应用在错误的地方。就像不是每个字符串都需要提取出来并转化为参数一样,也不是每个低级行为都需要从高级对象中提取出来并转化为可注入的依赖项。

这更像是吹毛求疵。但是依赖注入的一个缺点是,它使开发工具更难推理和导航代码。

具体来说,如果你在代码中控制-单击/命令-单击方法调用,它将带你到接口上的方法声明,而不是具体的实现。

这实际上是松散耦合代码(由接口设计的代码)的缺点,即使不使用依赖注入(即,即使只是使用工厂)也适用。但是依赖注入的出现真正鼓励了松耦合代码的普及,所以我想我应该提到它。

而且,松散耦合代码的好处远远超过这一点,因此我称之为吹毛求疵。尽管我工作了很长时间,知道如果您试图引入依赖注入,可能会遇到这种情况。

事实上,我敢大胆地猜测,对于依赖注入的每一个“缺点”,您都会发现许多优点远远超过它。

Constructor-based依赖注入(没有神奇的“框架”的帮助)是构造OO代码的一种干净而有益的方式。在我所见过的最好的代码库中,经过多年与Martin Fowler的其他前同事一起度过的时间,我开始注意到大多数以这种方式编写的好类最终都只有一个doSomething方法。

那么,主要的缺点是,一旦您意识到这只是一种笨拙的、冗长的OO方式,将闭包编写为类,以获得函数式编程的好处,那么您编写OO代码的动机就会迅速消失。

两件事:

  • 它们需要额外的工具支持来检查配置是否有效。

例如,IntelliJ(商业版)支持检查Spring配置的有效性,并将标记出配置中的类型违反等错误。如果没有这种工具支持,就无法在运行测试之前检查配置是否有效。

这就是为什么“蛋糕”模式(Scala社区所熟知)是一个好主意的原因之一:组件之间的连接可以由类型检查器检查。而注释或XML则没有这种好处。

  • 这使得程序的全局静态分析非常困难。

像Spring或Guice这样的框架很难静态地确定容器创建的对象图将是什么样子。尽管它们在容器启动时创建了一个对象图,但它们没有提供有用的api来描述将要创建的对象图。

误以为仅仅通过实现依赖注入就实现了代码的解耦,而实际上并没有解耦。我认为这是DI最危险的地方。

当您不断地使用技术来处理静态类型时,静态类型语言的假定好处似乎大大减少了。我刚刚采访的一家大型Java商店正在用静态代码分析绘制他们的构建依赖关系……它必须解析所有的Spring文件才能有效。

没有任何DI的代码会面临众所周知的陷入意大利面条式代码的风险——一些症状是类和方法太大,做太多,不容易更改、分解、重构或测试。

大量使用DI的代码可以是馄饨代码,其中每个小类就像一个单独的馄饨块——它做一件小事,并且坚持单一责任原则,这是很好的。但是从类本身来看,很难看到系统作为一个整体在做什么,因为这取决于所有这些小部分是如何组合在一起的,这是很难看到的。它看起来就像一大堆小东西。

通过避免大型类中大量耦合代码的意大利面复杂性,您将面临另一种复杂性的风险,其中有许多简单的小类,它们之间的交互非常复杂。

我不认为这是一个致命的缺点- DI仍然是非常值得的。在某种程度上,小班级只做一件事的馄饨风格可能是好的。即使过度,我也不认为它像意大利面条代码那样糟糕。但是要避免它,第一步是要意识到它可能会走得太远。点击链接了解如何避免这种情况。

在过去的6个月里,我一直在广泛使用Guice (Java DI框架)。虽然总的来说我认为它很棒(特别是从测试的角度来看),但也有一些缺点。最值得注意的是:

  • 依赖注入可以用在非常…创意……的方式。例如,我刚刚遇到了一些代码,使用自定义注释注入某个IOStreams(例如:@Server1Stream, @Server2Stream)。虽然这确实有效,而且我承认它具有一定的优雅性,但它使理解Guice注入成为理解代码的先决条件。
  • 学习项目时的学习曲线更高。这与第一点有关。为了理解使用依赖注入的项目是如何工作的,您需要理解依赖注入模式和特定的框架。当我开始我现在的工作时,我花了好几个小时搞清楚Guice在幕后做什么。
  • 构造函数变得很大。虽然这可以在很大程度上解决默认构造函数或工厂。
  • 我最近的例子是我在2个旗帜名称上发生了冲突。Guice默默地接受了这个错误,我的一个标志没有初始化。
  • 错误被推到运行时。如果你配置Guice模块不正确(循环引用,坏绑定,…)大多数错误在编译时不会被发现。相反,当程序实际运行时,错误就会暴露出来。

既然我已经抱怨过了。让我说,我将继续在我当前的项目中(很可能是在下一个项目中)使用Guice。依赖注入是一种非常强大的模式。但它肯定会让人困惑,无论你选择什么依赖注入框架,你几乎肯定会花一些时间咒骂。

另外,我同意其他发帖者的观点,依赖注入可能被过度使用。

有一件事让我对DI有点不安,那就是假设所有注入的对象都是实例化很简单无副作用 - or——依赖关系使用得如此频繁,以至于它超过了任何相关的实例化成本。

当依赖项在消费类中没有使用经常时,这可能很重要;比如IExceptionLogHandlerService。显然,这样的服务很少在类中调用(希望:))——大概只在需要记录异常时调用;然而规范构造器注入模式

Public Class MyClass
Private ReadOnly mExLogHandlerService As IExceptionLogHandlerService


Public Sub New(exLogHandlerService As IExceptionLogHandlerService)
Me.mExLogHandlerService = exLogHandlerService
End Sub


...
End Class

...要求提供该服务的“活动”实例,该死的成本/副作用。并不是说它可能会这样做,但是如果构建这个依赖实例涉及到服务/数据库命中,或配置文件查找,或锁定资源,直到释放该怎么办?如果该服务是按需构建的、服务定位的或工厂生成的(它们都有各自的问题),那么您将只在必要时才承担构建成本。

现在,一个普遍接受的软件设计原则是,构造一个对象很便宜,而会产生副作用。虽然这是一个很好的概念,但并不总是如此。然而,使用典型的构造器注入基本上要求这种情况。这意味着当您创建依赖项的实现时,必须在设计时考虑到依赖注入。为了在其他地方获得好处,您可能会使对象构造的成本更高,但是如果要注入这个实现,它可能会迫使您重新考虑该设计。

顺便说一下,某些技术可以通过允许延迟加载注入的依赖项来缓解这个问题,例如提供一个Lazy<IService>实例类作为依赖项。这将改变依赖对象的构造函数,并使其更加了解实现细节,例如对象构造开销,这也可以说是不可取的。