关于设计模式:使用依赖注入有什么缺点?

What are the downsides to using Dependency Injection?

我在这里尝试将DI作为一种模式引入工作中,我们的一个主要开发人员想知道:如果有的话,使用依赖注入模式的缺点是什么?

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

澄清:我说的是依赖注入模式(参见MartinFowler的这篇文章),不是特定的框架,无论是基于XML(如Spring)还是基于代码(如Guice)或"自滚动"。

编辑:在这里进行一些很好的进一步讨论/演讲/辩论/r/编程。


以下几点:

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

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


对于面向对象编程、样式规则和其他几乎所有东西,您经常会遇到同样的基本问题。事实上,做太多的抽象和添加太多的间接性是可能的——非常常见的——并且通常在错误的地方过度地应用好的技术。

您应用的每个模式或其他构造都会带来复杂性。抽象和间接会分散信息,有时会将不相关的细节移开,但同样地,有时会使准确理解正在发生的事情变得更加困难。你应用的每一条规则都会带来僵化,排除那些可能只是最佳方法的选择。

关键是要编写代码来完成这项工作,并且是健壮的、可读的和可维护的。你是个软件开发人员,而不是象牙塔的建设者。

相关环节

http://thedailywtf.com/articles/the"内部平台"effect.aspx

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

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

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

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

1
2
3
4
void Say_Hello_World ()
{
  std::cout <<"Hello World" << std::endl;
}

我有一个依赖,我想提取和注入-文本"你好世界"。足够简单…

1
2
3
4
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

这些"依赖性"不仅仅是原始开发人员的问题——使用该接口的每个人都必须查找依赖性是什么,它们是如何指定的,以及它们的含义,并为其应用程序确定要做什么。在这里,"合理的违约"可以使生活简单得多。

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

所有这些都不应该被理解为声称依赖注入是不好的——离它很远。但是任何好的技术都可能被过度地应用在错误的地方。正如不是每个字符串都需要被提取出来并转换成参数一样,不是每个低级行为都需要从高级对象中提取出来并转换成可注入的依赖关系。


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

  • 学习需要时间
  • 如果误会,会带来更多的伤害而不是好处。
  • 如果采取极端的做法,它可能比证明利益更有效。


控制反转的最大"缺点"(不是很DI,但足够接近)是它倾向于去掉一个单一的点来查看算法的概述。不过,这基本上就是当您拥有分离的代码时会发生的事情——在一个地方查看的能力是紧密耦合的产物。


我认为不存在这样的列表,但是请尝试阅读这些文章:

  • DI可以模糊代码(如果您没有使用好的IDE)

  • 根据鲍勃叔叔的说法,滥用IOC会导致错误的代码。

  • 需要注意过度工程和创造不必要的多功能性。


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

  • 代码可能变得难以理解。依赖注入可用于非常…创意…方法。例如,我刚刚遇到一些代码使用自定义注释来注入特定的iostream(例如:@server1stream,@server2stream)。虽然这确实有效,而且我承认它有一定的优雅,但它使理解guice注入成为理解代码的先决条件。
  • 学习项目时有较高的学习曲线。这与第1点有关。为了理解使用依赖注入的项目如何工作,您需要了解依赖注入模式和特定框架。当我开始我目前的工作时,我花了很多困惑的时间在幕后摸索基斯在做什么。
  • 建设者变大了。尽管这在很大程度上可以通过默认的构造函数或工厂来解决。
  • 错误可以被混淆。我最近的一个例子是我在2个标志名上发生了冲突。Guice默默地接受了错误,我的一个标志没有初始化。
  • 将错误推送到运行时。如果您的guice模块配置不正确(循环引用、错误绑定等),那么大多数错误在编译期间都不会被发现。相反,当程序实际运行时,错误会暴露出来。

现在我已经抱怨了。让我说,我将继续(自愿地)在我当前的项目中使用Guice,很可能是我的下一个项目。依赖注入是一种非常强大的模式。但它肯定会令人困惑,而且您几乎肯定会花一些时间诅咒您选择的任何依赖注入框架。

另外,我同意其他海报,依赖注入可以被过度使用。


没有任何DI的代码会有陷入意面代码的风险——一些症状是类和方法太大、做得太多并且不容易更改、分解、重构或测试。

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

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

我不认为这是一个致命的缺点-DI仍然非常值得。某种程度上的饺子式小班级只做一件事可能是好的。即使过分了,我也不认为它像意大利面代码那样糟糕。但是,意识到可以走得太远是避免这种情况发生的第一步。按照链接讨论如何避免它。


如果您有一个自行开发的解决方案,那么依赖项就在您的构造函数中。或者作为方法参数,这也不太难发现。尽管框架管理的依赖关系,如果走到极端,可能会开始看起来像魔术。

然而,在太多的类中有太多的依赖项是一个明显的迹象,表明您的类结构已经被搞砸了。因此,在某种程度上依赖注入(自主开发或框架管理)可以帮助解决隐藏在黑暗中的引人注目的设计问题。


为了更好地说明第二点,以下是本文(原始资料)的一个摘录,我衷心地认为这是构建任何系统的根本问题,而不仅仅是计算机系统。

Suppose you want to design a college campus. You must delegate some of the design to the students and professors, otherwise the Physics building won't work well for the physics people. No architect knows enough about about what physics people need to do it all themselves. But you can't delegate the design of every room to its occupants, because then you'll get a giant pile of rubble.

How can you distribute responsibility for design through all levels of a large hierarchy, while still maintaining consistency and harmony of overall design? This is the architectural design problem Alexander is trying to solve, but it's also a fundamental problem of computer systems development.

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


仅仅通过实现依赖注入而实际上没有将其分离,从而实现了代码分离的假象。我认为这是DI最危险的事情。


有一件事让我对DI有点不安,那就是假设所有注入的对象都很便宜,不会产生任何副作用——或者说,依赖关系被频繁使用,以至于它超过了任何相关的实例化成本。

当依赖项在消费类中不经常使用时,例如IExceptionLogHandlerService之类的东西,这一点可能很重要。显然,这样的服务很少在类中被调用(希望是:)-大概只在需要记录的异常上调用;但是规范的构造函数注入模式…

1
2
3
4
5
6
7
8
9
Public Class MyClass
    Private ReadOnly mExLogHandlerService As IExceptionLogHandlerService

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

     ...
End Class

…要求提供此服务的"实时"实例,该死的成本/副作用。不太可能,但是如果构造这个依赖实例涉及服务/数据库命中、配置文件查找,或者在释放之前锁定了一个资源,该怎么办?如果这项服务是按需要建造的,服务是定位的,或者是工厂生产的(所有这些都有自己的问题),那么只有在必要的时候,你才会承担建造成本。

现在,构建一个对象既便宜又不产生副作用,这是一个公认的软件设计原则。虽然这是个不错的想法,但情况并非总是如此。但是,使用典型的构造函数注入基本上需要这样做。这意味着,当您创建依赖项的实现时,必须在设计时考虑DI。也许为了在其他地方获得利益,您可能会使对象构造成本更高,但是如果要注入这个实现,它可能会迫使您重新考虑该设计。

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


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

具体来说,如果您在代码中控制click/command单击一个方法调用,它将把您带到接口上的方法声明,而不是具体的实现。

这实际上是松散耦合代码(由接口设计的代码)的一个缺点,即使您不使用依赖注入(即,即使您只是使用工厂),也适用。但是依赖注入的出现确实鼓励了松散耦合的代码向大众开放,所以我想我会提到它。

另外,松耦合代码的好处远远超过了这一点,因此我称之为吹毛求疵。尽管我已经工作了足够长的时间,知道如果您尝试引入依赖注入,这可能是一种推回。

实际上,我冒昧地猜测,对于依赖注入所能找到的每一个"缺点",您都会发现许多好处远远超过它。


基于构造函数的依赖注入(不借助于神奇的"框架")是构建OO代码的一种干净而有益的方法。在我所见过的最好的代码库中,多年来,我和马丁·福勒的其他前同事一起,开始注意到大多数这样编写的好类最终都有一个单一的doSomething方法。

那么,主要的缺点是,一旦你意识到这仅仅是一种笨拙的将闭包作为类来编写的OO方法,为了获得函数式编程的好处,你编写OO代码的动机很快就会消失。


我发现构造器注入会导致非常难看的构造器,(并且我在代码库中使用它——也许我的对象太细粒度了?).此外,有时在构造函数注入中,我会得到可怕的循环依赖(尽管这很少见),因此您可能会发现自己必须在更复杂的系统中拥有某种准备就绪的状态生命周期和多轮依赖注入。

但是,我更喜欢construtor注入而不是setter注入,因为一旦构建了对象,我就毫无疑问地知道它处于什么状态,是在单元测试环境中,还是装载在某个ioc容器中。它,以一种迂回的方式,是说,我认为是塞特注射的主要缺点。

(作为旁注,我确实觉得整个主题非常"宗教化",但您的里程数将随着开发团队的技术热情水平而变化!)


如果您使用的是不带IOC容器的DI,那么最大的缺点就是您很快就会看到代码实际上有多少依赖项,以及它们之间的耦合有多紧密。(但我认为这是一个好的设计!)自然的进展是朝着一个IOC容器前进,它需要一点时间来学习和实现(并没有WPF学习曲线那么糟糕,但也不是免费的)。最后一个缺点是,一些开发人员将开始编写诚实可靠的单元测试,这将需要他们一些时间来解决。以前可以在半天内完成某件事情的开发人员会突然花费两天时间来尝试如何模拟他们所有的依赖关系。

与MarkSeemann的回答类似,底线是你花时间成为一个更好的开发人员,而不是把代码的一部分拼凑在一起,然后把它扔出去/投入生产。你的生意更喜欢哪一个?只有你能回答。


DI是一种技术或模式,与任何框架都不相关。您可以手动连接依赖项。DI帮助您处理SR(单一责任)和SOC(分离关注点)。DI带来了更好的设计。从我的观点和经验来看,没有什么缺点。像其他模式一样,您可能会弄错或误用它(但DI的情况非常困难)。

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

  • 你必须训练你的球队
  • 您必须更改您的申请(包括风险)

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


代码可读性。由于依赖项隐藏在XML文件中,所以您将无法轻松地找出代码流。


两件事:

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

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

这就是为什么"cake"模式(scala社区都知道)是一个好主意的原因之一:可以通过类型检查器检查组件之间的连接。注释或XML并没有这样的好处。

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

像Spring或Guice这样的框架使静态地确定由容器创建的对象图是什么样子变得困难。尽管它们在容器启动时创建了一个对象图,但它们不提供描述已创建/将要创建的对象图的有用API。


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


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