使用dependency injection有什么缺点?

我试图在工作中引入DI作为模式,我们的一位开发人员想知道:使用dependency injection模式有什么缺点

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


澄清 :我正在谈论dependency injection模式 (请参阅Martin Fowler的本文 ), 而不是基于XML(如Spring)或基于代码(如Guice)或“自动滚动” 。


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

几点:

  • DI增加了复杂性,通常通过增加class级的数量,因为责任分离更多,这并不总是有益的
  • 您的代码将(有点)耦合到您使用的dependency injection框架(或者更一般地,您决定如何实现DI模式)
  • 执行typesparsing的DI容器或方法通常会导致运行时间的轻微损失(可以忽略不计,但在那里)

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

你经常用面向对象的编程,风格规则和其他所有东西来获得同样的基本问题。 事实上,这是可能的,这是很常见的 – 做太多的抽象,增加太多的间接性,并且通常在错误的地方过度使用好的技术。

您应用的每种模式或其他构造都会带来复杂性 抽象和间接消散了周围的信息,有时候会将不相关的细节排除在外,但同样有时会使得难以准确理解发生的事情。 你应用的每一条规则都会带来灵活性,排除那些可能是最好的方法。

关键是编写能够完成这个工作的代码,并且是健壮的,可读的和可维护的。 你是一个软件开发者,而不是象牙塔build设者。

相关链接

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

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


可能最简单的dependency injectionforms(不要笑)是一个参数。 从属代码依赖于数据,并且通过传递参数来注入数据。

是的,这是愚蠢的,它没有解决dependency injection的面向对象的点,但function程序员会告诉你(如果你有第一类function),这是你唯一需要的dependency injection。 这里要说的是一个微不足道的例子,并展示潜在的问题。

让我们来看看这个简单的传统函数 – 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。 但这意味着我的string必须是wchar_t,而不是char。 要么每个调用者都被改变,要么(更合理地),旧的实现被replace为一个转换string并调用新的实现的适配器。

那就是维护工作,如果我们保留原来的话就不需要维护。

如果它看起来微不足道,看看从Win32 API这个现实世界的function…

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

那些“依赖”不仅仅是原始开发者的一个问题 – 每个使用这个接口的人都必须查找依赖关系是什么,它们是如何指定的以及它们是什么意思,以及为他们的应用程序做些什么。 这是“明智的默认”这个词可以使生活变得更简单的地方。

原则上面向对象的dependency injection是没有区别的。 编写一个类是开销,无论是在源代码文本还是在开发者时间,如果这个类是根据一些依赖对象的规范来编写依赖关系的,那么依赖对象就被locking在支持这个接口的地方,即使有需要取代该对象的实现。

这些都不应该被视为声称dependency injection是不好的 – 远非如此。 但是任何好的技术都可以被过度地应用在错误的地方。 就像并不是每一个string都需要被提取出来并转换成一个参数一样,并不是每一个低层次的行为都需要从高层次的对象中提取出来,变成一个可注入的依赖。

这是我自己的最初反应:基本上是任何模式的相同缺点。

  • 学习需要时间
  • 如果误解了会导致更多的伤害而不是好事
  • 如果采取了极端的话,那么可以做更多的工作,而不是certificate这个好处

控制反转的最大“缺点”(不完全是DI,但足够接近)是,它倾向于删除一个单一的点来看一个algorithm的概述。 这基本上是在你分离代码的时候会发生什么事情 – 在一个地方查看的能力是一个紧密耦合的产物。

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

  • DI可以掩盖代码(如果你没有使用一个好的IDE)

  • 根据Bob叔叔的说法,误用IoC可能导致错误的代码。

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

过去6个月我一直广泛使用Guice(Java DI框架)。 总的来说,我认为这是很好的(特别是从testing的angular度来看),但是也有一些缺点。 最为显着地:

  • 代码可能变得更难理解。 dependency injection可以用于非常有创意的方式。 例如,我刚刚遇到一些代码,使用自定义注释来注入某些IOStreams(例如:@ Server1Stream,@ Server2Stream)。 虽然这是行得通的,而且我承认有一定的优雅,但是理解Guice注入是理解代码的先决条件。
  • 学习项目时学习曲线较高。 这与第1点有关。为了理解使用dependency injection的项目是如何工作的,你需要理解dependency injection模式和特定的框架。 当我开始当前的工作时,我花了相当多的时间来处理Guice在幕后做的事情。
  • build设者变得庞大。 虽然这可以用默认的构造函数或工厂很大程度上解决。
  • 错误可能会被混淆。 我最近的例子是我碰到了两个国旗。 Guice默默吞下了错误,我的一个标志没有被初始化。
  • 错误被推到运行时间。 如果你错误地configuration你的Guice模块(循环引用,错误的绑定,…),大部分的错误在编译期间都没有被发现。 相反,当程序实际运行时,这些错误是暴露的。

现在我已经抱怨了。 让我说,我会继续(愿意)在我目前的项目中使用Guice,很可能是我的下一个项目。 dependency injection是一个伟大的和令人难以置信的强大的模式。 但它肯定可能会让人困惑,你几乎肯定会花一些时间诅咒你select的dependency injection框架。

另外,我同意其他海报,dependency injection可以被滥用。

没有任何DI的代码运行已知的风险,纠缠成意大利面条代码 – 一些症状是,类和方法太大,做太多,不能轻易改变,分解,重构或testing。

DI代码使用了很多可以是馄饨代码 ,其中每个小类像个人馄饨块 – 它做一件小事,坚持单一责任原则 ,这是很好的。 但是单独看课,很难看出整个系统是怎么做的,因为这取决于所有这些小部件如何配合在一起,这是很难看到的。 它看起来像是一大堆小东西。

通过避免在一个大类中耦合代码的大部分的意大利面条复杂性,你会冒另一种复杂的风险,那里有许多简单的小类,而且它们之间的交互是复杂的。

我不认为这是一个致命的缺点 – DI仍然是非常值得的。 某种程度的饺子风格与小class只做一件事可能是好的。 即使过量,我不认为这是不好的意大利面代码。 但是要意识到可以采取的措施是避免它的第一步。 按照链接讨论如何避免它。

如果你有一个本土的解决scheme,依赖关系在你的构造函数中是正确的。 或者,也许作为方法参数,再次不难看出。 虽然框架pipe理的依赖,如果采取极端,可以开始出现像魔术。

然而,在太多的类中依赖太多是一个明显的迹象,你是类结构搞砸了。 因此,dependency injection(本地或框架pipe理)可以帮助将明显的devise问题带出来,否则这些问题可能会隐藏在黑暗中。


为了更好地说明第二点,下面是这篇文章的摘录( 原始资料 ),我完全相信这是build立任何系统的根本问题,而不仅仅是计算机系统。

假设你想devise一个大学校园。 你必须把一些devise委派给学生和教授,否则物理学的build筑物对于物理学家来说就不太好。 没有build筑师知道什么物理人需要自己做这一切。 但是你不能把每个房间的devise都委托给它的居住者,因为那样你会得到一堆巨大的碎石。

你怎样才能把devise的责任分配到各个层级,同时保持整体devise的一致性和和谐? 这是亚历山大试图解决的build筑devise问题,但这也是计算机系统开发的一个基本问题。

DI解决了这个问题吗? 没有 。 但是,如果您将每个房间的devise责任委托给居住者,它确实帮助您清楚地看到。

我发现构造函数注入可能会导致大的丑陋的构造函数,(我在整个代码库中使用它 – 也许我的对象太细粒度?)。 另外,有时在构造函数注入的时候,我最终会遇到可怕的循环依赖(尽pipe这很less见),所以你可能会发现自己必须在更复杂的系统中进行几轮dependency injection才能拥有某种状态生命周期。

然而,我赞成build造者注入的build造者注入,因为一旦我的对象被创build,我毫无疑问知道它是处于什么状态,无论是在unit testing环境还是装载在一些国际奥委会容器中。 其中,以一种迂回的方式,是说我觉得是二传注射的主要缺点。

(作为旁注,我确实发现整个话题都很“虔诚”,但是你的里程会随着你的开发团队的技术狂热水平而变化!)

这是更挑剔的。 但是dependency injection的一个缺点就是它使得开发工具难以推理和导航代码。

具体来说,如果你在代码中使用Control-Click / Command-单击一个方法调用,它将把你带到一个接口上的方法声明,而不是具体的实现。

这实际上是松耦合代码(由接口devise的代码)的一个缺点,即使你不使用dependency injection(即使你只是使用工厂)也适用。 但是,dependency injection的出现实际上是鼓励松散耦合的代码给大众,所以我想我会提到它。

另外,松耦合代码的好处远远超过了这个,所以我把它称为挑剔。 尽pipe我已经工作了很长时间,知道如果你尝试引入dependency injection,这可能是一种推回。

事实上,我敢于猜测,对于dependency injection的每一个“缺点”,你都会发现许多远远超过它的好处。

基于构造器的dependency injection(不借助神奇的“框架”)是构造OO代码的一种干净且有益的方式。 在我见过的最好的代码库中,经过多年与Martin Fowler的其他前同事的共同努力,我开始注意到用这种方式编写的大多数好的类最终都只有一个doSomething方法。

然而,主要的缺点是,一旦你意识到这只是一个长期的OO方式,为了获得函数式编程的好处而把闭包写成类,你写OO代码的动机可以很快消失。

有一件事让我对DI感到一丝茫然,那就是假设所有注入的对象都很便于实例化,并且不产生副作用。或者 ,依赖关系被频繁使用,使得它超过了任何相关的实例化成本。

在消费类中不常使用依赖的情况下,这可能是重要的。 比如像IExceptionLogHandlerService这样的东西。 很显然,像这样的服务很less在类中调用(希望:)),大概只有在需要logging的exception情况下。 但规范的构造函数注入模式

 Public Class MyClass Private ReadOnly mExLogHandlerService As IExceptionLogHandlerService Public Sub New(exLogHandlerService As IExceptionLogHandlerService) Me.mExLogHandlerService = exLogHandlerService End Sub ... End Class 

…要求提供这种服务的“实时”实例,以此来达到所需的成本/副作用。 并不是这样,但是如果构造这个依赖实例涉及到服务/数据库命中,或者configuration文件查找,或者locking了一个资源直到被处理掉,该怎么办呢? 如果这个服务是根据需要build造的,服务位置的还是工厂生成的(都有自己的问题),那么只有在必要的时候才会承担build设成本。

现在,构build一个对象的软件devise原则是普遍接受的, 而且不会产生副作用。 虽然这是一个很好的概念,但情况并非总是如此。 然而,使用典型的构造函数注入基本上要求这种情况。 意思是当你创build一个依赖的实现时,你必须用DI来devise它。 也许你会为了在其他地方获得好处而使对象构造更加昂贵,但是如果这个实现将被注入,它可能会迫使你重新考虑这个devise。

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

如果您在没有IOC容器的情况下使用DI,最大的缺点是您很快会看到您的代码实际上具有多less依赖关系,以及每个事物的紧密耦合程度如何。 (“但我认为这是一个很好的devise!”)自然的进展是走向一个国际奥委会的容器,可以花一点时间来学习和实施(不像WPF的学习曲线一样坏,但它不是免费的其一)。 最后的缺点是一些开发人员会开始编写诚实的unit testing,并且需要时间来弄明白。 以前可能会在半天之内搞清楚什么的开发人员会突然花费两天的时间试图弄清楚如何去模拟所有的依赖关系。

与Mark Seemann的回答类似,底线是你花费时间成为一个更好的开发者,而不是把代码拼凑在一起,把它扔到门外/投入生产。 你的生意有哪些? 只有你可以回答。

你只是通过实现dependency injection来解耦你的代码,而没有实际解耦它。 我认为这是DI最危险的事情。

DI是一种技术或模式,与任何框架无关。 你可以手动连接你的依赖关系。 DI可以帮助您完成SR(单一责任)和SoC(关注点分离)。 DI导致更好的devise。 从我的观点和经验来看, 没有任何缺点 。 像其他任何模式一样,你可能会弄错或误用它(但是DI的情况很难)。

如果您将DI作为原则引入传统应用程序,请使用框架 – 您可以做的最大的一个错误就是将其误用为Service-Locater。 DI +框架本身非常好,只要我看到它就让事情变得更好! 从组织的angular度来看,每一个新的过程,技术,模式,都有一个共同的问题:

  • 你必须训练你的团队
  • 你必须改变你的申请(包括风险)

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

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

两件事情:

  • 他们需要额外的工具支持来检查configuration是否有效。

例如,IntelliJ(商业版)支持检查Springconfiguration的有效性,并在configuration中标记types违规等错误。 如果没有这种工具支持,在运行testing之前无法检查configuration是否有效。

这就是为什么“蛋糕”模式(如Scala社区所知)是一个好主意的原因:组件之间的连线可以通过types检查器来检查。 您没有注释或XML的好处。

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

像Spring或Guice这样的框架很难静态确定容器创build的对象图是什么样的。 虽然他们在容器启动时创build了一个对象图,但是他们没有提供描述将要创build的对象图的有用的API。

当你不断地使用技术来解决静态types问题时,似乎静态types语言的假设好处会大大减less。 我刚刚采访过的一家大型Java商店正在使用静态代码分析来映射他们的构build依赖关系……为了有效parsing所有的Spring文件。

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