我在这里尝试将DI作为一种模式引入工作中,我们的一个主要开发人员想知道:如果有的话,使用依赖注入模式的缺点是什么?
注意,我在这里寻找一个尽可能详尽的列表,而不是关于这个主题的主观讨论。
澄清:我说的是依赖注入模式(参见MartinFowler的这篇文章),不是特定的框架,无论是基于XML(如Spring)还是基于代码(如Guice)或"自滚动"。
编辑:在这里进行一些很好的进一步讨论/演讲/辩论/r/编程。
- 指定我们应该讨论DI本身还是支持它的特定类型的工具(基于XML还是不基于XML),这可能是一个好主意。
- DI作为一个模式和一个特定的框架有什么区别?介意解释一下吗?
- 区别在于,我不想寻找只适用于特定框架的答案,比如"XML糟糕"。:)我正在寻找适用于DI概念的答案,如Fowler在这里概述的那样:martinfowler.com/articles/injection.html
- 一般来说,我看不到DI的任何缺点(构造函数、setter或方法)。它实现了解耦和简化,无需开销。
- @除了塞特人以外,基萨基还注射了一些东西。最好是制造在建筑中可用的物体。如果您不相信我,只需尝试使用"setter"注入向对象添加另一个合作者,您肯定会得到一个NPE……
- 为服务定位器辩护
- 所有复杂的框架都是缺点。"在这里注册容器,"不要触摸它","您没有使用这种类型的应用程序(WPF)所需的神奇的、无文档记录的模式创建该实例"。导致浪费了数小时来弄清楚绑定为什么不起作用。如果有人能做一个理智的人,只要在需要的时候制造物品,而且是可以预测的,我可能会为此付钱。
- @在没有class语法的情况下实现设计模式的epaga将是澄清此类问题的基本方法。这是DI实现的一个例子。用英语文学来回答这些问题,总是自己的解释而不是别的。
以下几点:
- DI增加了复杂性,通常通过增加类的数量来增加,因为职责分离得更多,这并不总是有益的。
- 您的代码将(在一定程度上)耦合到您使用的依赖注入框架(或者更普遍地说,您决定如何实现DI模式)。
- 执行类型解析的DI容器或方法通常会受到轻微的运行时惩罚(可以忽略不计,但确实如此)
通常,分离的好处使每个任务更容易阅读和理解,但增加了编排更复杂任务的复杂性。
- -类的分离减少了复杂性。许多类不会使应用程序变得复杂。-您应该只在应用程序根目录下依赖于DI框架。
- 我们是人类;我们的短期记忆是有限的,不能同时处理许多问题。对于类,它与用于开发程序的方法、函数、文件或任何构造的相同。
- @是的,但是5个真正复杂的类并不比15个简单的类简单。降低复杂性的方法是减少程序员处理代码所需的工作集——复杂的、高度相互依赖的类不能做到这一点。
- @我想我们都同意这一点。请记住,复杂性与耦合不同。减少耦合会增加复杂性。我并不是说DI是一件坏事,因为它增加了类的数量,我强调了与之相关的潜在缺点。:)
- 基本上,它减少了每个单独的构建块的复杂性,但增加了用于构建墙的构建块的数量。
- @哈瓦德:公平的。它的一部分也是您如何定义复杂性的——我通常将其定义为程序员所需的最小工作集。
- +1对于"DI增加了复杂性",在DI和其他方面都是如此。(几乎)只要我们增加灵活性,就会增加复杂性。这都是关于平衡的,从了解好的方面和坏的方面开始。当人们说"没有坏处"时,这是一个确定的指标,表明他们还没有完全理解这件事。
- 是啊。今天,我在五分钟内放弃了一个主要特性,因为我所要做的就是删除一些类并修改我的对象图。由于没有实际的逻辑被改变,所以很容易用高的置信度来做,并且丢失对象的编译器错误告诉了我需要改变什么。伙计,这种复杂性真让我恼火!
- 为了建立在@robert所说的基础上,DI从许多类中消除了布线依赖性形式的一点复杂性,并将其集中起来。它还使每个测试不那么复杂。布线的新地方可能有些复杂,但总体上DI应该大大降低复杂性。如果您认为DI增加了复杂性,那么您可能是做错了。
- EEH?我觉得每个人都得到了DI的错误。DI不需要框架。没有开销。另外,如果您已经有足够的去耦代码(希望是这样!)使用DI将不会再添加任何类。这就是如何将依赖项传递给对象。使用DI,您将把它们传递给对象(在构造、setter或方法上),而不是类中的硬编码。这不应该产生开销或复杂性。它简化和分离。
- @基萨基:谢谢!我想我错了,因为我只是通过依赖,而不是使用框架;)
- @H&229;Vard S:我同意Kyoryu的观点:当你在一个类上工作并且它是分离的时,你需要记住的东西要少得多:你知道你正在工作的类,你知道它们的合同。你根本不关心你一年前添加的这个小私有成员的状态,当你处理单块类的时候就是这样,或者更糟的是,当你使用有状态的单块类的时候!
- 您也可以认为,将长方法分解为短方法会增加复杂性。
- 你的问题总是有内在的复杂性。在设计构图的时候,你必须预先考虑复杂度,然后决定现在要解决多少,要投入多少,要做多少开放式结尾,等等。这并不是说构图(di)是解决问题的唯一方法,但最好现在面对复杂度,而不是在生产中,当你意识到你把所有的模型都弄错了,但是嘿,数据库调用真的很快!
- When people say, 'there are no downsides,' it's a sure indicator that they haven't fully understood the thing yet.这是一个毫无根据的主张——只有与某些事物相比,缺点才能存在。与传统意大利面相比,执行良好的DI的唯一缺点是"它没有提供那么多工作保障"。
- 这里上票太多了。真正的答案是安迪派克下面的答案(为了您的方便:stackoverflow.com/a/4988337/414058)。依赖注入的最大缺点是它将强类型语言转换为动态语言,并带来动态语言的所有缺点:没有IDE自动化,错误被推送到运行时,无法判断是否使用了给定的注册(很难保持代码干净)。选择一种语言是有反作用的,因为它是静态检查的,然后为主要控制流方面选择任何基于反射的方法。
对于面向对象编程、样式规则和其他几乎所有东西,您经常会遇到同样的基本问题。事实上,做太多的抽象和添加太多的间接性是可能的——非常常见的——并且通常在错误的地方过度地应用好的技术。
您应用的每个模式或其他构造都会带来复杂性。抽象和间接会分散信息,有时会将不相关的细节移开,但同样地,有时会使准确理解正在发生的事情变得更加困难。你应用的每一条规则都会带来僵化,排除那些可能只是最佳方法的选择。
关键是要编写代码来完成这项工作,并且是健壮的、可读的和可维护的。你是个软件开发人员,而不是象牙塔的建设者。
相关环节
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
这些"依赖性"不仅仅是原始开发人员的问题——使用该接口的每个人都必须查找依赖性是什么,它们是如何指定的,以及它们的含义,并为其应用程序确定要做什么。在这里,"合理的违约"可以使生活简单得多。
原则上,面向对象的依赖注入没有什么不同。编写一个类是一种开销,无论是在源代码文本还是在开发人员时间,如果该类是根据一些依赖对象规范编写来提供依赖项的,那么依赖对象就被锁定为支持该接口,即使需要替换该对象的实现。
所有这些都不应该被理解为声称依赖注入是不好的——离它很远。但是任何好的技术都可能被过度地应用在错误的地方。正如不是每个字符串都需要被提取出来并转换成参数一样,不是每个低级行为都需要从高级对象中提取出来并转换成可注入的依赖关系。
- 依赖注入没有这些缺点。它只改变了将依赖项传递给对象的方式,这既不增加复杂性,也不增加灵活性。恰恰相反。
- @是的,它改变了你传递依赖的方式。您必须编写代码来处理依赖项的传递。在这样做之前,您必须找出哪些依赖项要传递,以一种对那些没有对相关代码进行编码的人进行定义的方式来定义它们。等等,您可以避免运行时的影响(例如,在C++中使用策略参数到模板),但这仍然是您必须编写和维护的代码。如果这是合理的,那是一个非常小的价格,一个巨大的回报,但如果你假装没有价格支付,这意味着你将支付的价格,当没有收益。
- @Kissaki——至于不灵活,一旦您指定了依赖项是什么,您就被锁定在这个范围内了——其他人已经根据这个规范编写了要注入的依赖项。如果您需要一个依赖代码的新实现,它需要稍微不同的依赖项——很难,您就被锁定了。是时候开始为这些依赖项编写一些适配器了(还有一点开销和另一个抽象层)。
- 如果考虑重载,我不确定是否理解这个示例。为了输出文本"hello world",我们使用()签名。要输出8位字符串,请使用(char)签名。要输出unicode,重载(wchar_t)。显然,在这种情况下,(wchar_t)是其他人在幕后称之为的。这里不需要太多的代码重写。我错过什么了吗?
- @配料-是的,这是一个简化的玩具示例。如果您真的这样做了,您只需要使用重载而不是适配器,因为复制代码的成本比适配器的成本要低。但请注意,复制代码通常是一件坏事,使用适配器来避免复制代码是另一种好技术(有时会使用太多的适配器,而且会出现在错误的地方)。
- 我明白你的意思。然而,这种问题可能表明抽象的滥用。根据维基百科:A good abstraction will generalize that which can be made abstract; while allowing specificity where the abstraction breaks down and its successful application requires customization to each unique requirement or problem.
- 我喜欢你的想法和例子,但我认为这是误导。您的原始函数没有依赖关系,它们是用DI插入的。您真的应该比较从某个地方获取值的函数(比如getter函数)和参数传递,否则比较是不公平的。如果要将文本更改为Unicode,还需要维护工作来更改getter函数。由于依赖关系的使用通常比给定的要多,因此DI的维护实际上应该比正常的依赖关系管理小。
- @Francescopasa——重点不是提倡另一种形式的依赖性管理——重点是不要因为"DI很棒"而出现依赖性管理问题。!,但仅在需要时。如果提供了DI,即使它是不必要的,但它是一个封装失败——应该是完全封装的实现细节已经以依赖关系的形式向调用者公开了,因此如果调用者改变了,它就可能被破坏。完全封装的实现细节不能导致定义上的破坏。调用者也无法控制权衡封装的内部。
- 我理解您的观点并完全同意,但是依赖与封装的问题更多的是依赖注入。
这是我自己的初步反应:基本上任何模式的缺点都是一样的。
- 学习需要时间
- 如果误会,会带来更多的伤害而不是好处。
- 如果采取极端的做法,它可能比证明利益更有效。
- 为什么要花时间学习?我认为与EJB相比,它使事情变得简单。
- 考虑到你不想进行主观讨论,这似乎是手摇。我建议(如有必要)建立一个现实的考试榜样。在没有DI的情况下构建一个样本将是一个很好的起点。然后我们可以向IT W.R.T.挑战需求变更,并检查影响。(顺便说一下,这对我来说非常方便,因为我正要睡觉。)
- 这真的需要一些例子来支持它,并且我已经教了很多人,我可以告诉你,学习和开始实践它真的是一个非常简单的概念。
- 我唯一想说的是,它花了时间来学习如何"在DI中"思考——这是一种不同的(更好的)接近设计的方式。我不再允许类创建和获取它们自己的依赖项。对象树是从叶到根构建的,而不是从叶到根构建的。这需要一点习惯。这就是我花时间学习的意思。
- 我并不认为DI是一种模式。它(加上IOC)实际上更像一个编程模型——如果您完全遵循它们,您的代码看起来比"典型"的伪过程OO更像一个行动者。
- +我在使用symfony 2,DI覆盖了所有的用户代码,只是在使用它。
- 我并不认为DI/IOC是一种设计模式,就像我认为它是一种反模式一样。除非它在真正合适的地方被谨慎使用。有点像单件模式——要谨慎使用,并且只在增加实际价值的地方使用。不幸的是,现在我们看到了以DI/IOC为核心的整个框架。魔术-它是自动注射的,但嘿,我们到底注射了什么,它是如何到达那里的,谁知道,谁在乎-真的很糟糕的设计。
控制反转的最大"缺点"(不是很DI,但足够接近)是它倾向于去掉一个单一的点来查看算法的概述。不过,这基本上就是当您拥有分离的代码时会发生的事情——在一个地方查看的能力是紧密耦合的产物。
- 但可以肯定的是,"不利因素"是因为我们正在解决的问题的性质,将其脱钩,这样我们就可以轻松地更改实现,这意味着没有一个地方可以查看,它能够查看它有什么相关性?我能想到的唯一情况是调试,调试环境应该能够介入实现。
- 这就是为什么"不利"一词在引号中出现的原因:)松散耦合和强封装排除了"一个地方可以看到所有东西"的定义。如果你觉得我反对DI/IOC,请看我对哈瓦德的回应的评论。
- 我同意(我甚至投票支持你)。我只是在说明问题。
- @维克:我想缺点是人类很难理解。分离事物会增加复杂性,因为人类很难理解,并且需要更长的时间才能完全理解。
- 如果我们同意海报的意思是IOC而不是DI,那么我会说这是唯一的缺点。希望像代码气泡这样的工具可以使查看松散耦合的算法成为更好的体验。
- 因为DI导致去耦,而非DI导致耦合。可以说,与耦合相比,去耦有一些缺点,这正失去在单个位置查看算法概述的能力。在这种情况下,我会用比喻的方式说,作为一个人,我们有缺点,因为我们只有有限的大脑来处理复杂性的本质。
- 相反,DI的一个优点是,您可以查看单个类的构造函数,并立即计算出它所依赖的类(即,哪些类需要完成其工作)。对于其他代码来说,这要困难得多,因为代码可以随意创建类。
- 相反,@contango-DI的一个优点是完全混淆了应用程序逻辑流,特别是在非琐碎的应用程序中。有时甚至不再需要代码混淆(用于复制保护),因为没人能知道注入了什么、注入了什么以及它来自哪里。您将如何准确地调试到DI注入的构造函数中,并跟踪创建依赖项的方式和位置等逻辑…
- @tcwicks您确实有一点,一个结构糟糕的DI应用程序不会神奇地变得容易理解。然而,一个结构良好的DI应用程序非常适合使用。为了回答您的问题,如果您想在C中进行调试,我发现在任何地方放置一个断点,然后查看调用堆栈以查看代码是如何到达那里的,大多数时间都会回答我的大多数问题。而且,添加"toString"重写来总结调试器鼠标悬停时每个类的状态也是非常有用的。
我认为不存在这样的列表,但是请尝试阅读这些文章:
DI可以模糊代码(如果您没有使用好的IDE)
根据鲍勃叔叔的说法,滥用IOC会导致错误的代码。
需要注意过度工程和创造不必要的多功能性。
- 我很好奇,当有人问wonsides时,为什么要用"没有"的精神回答,而那些包含一些与问题无关的信息的回答呢?
- -1因为我不寻找链接集合,所以我在寻找实际的答案:如何总结每一篇文章的要点?然后我会投反对票。注意,文章本身很有趣,尽管前两篇文章实际上是在揭穿DI的负面影响?
- 我想知道最乐观的答案是否也被否决了,因为它根本没有回答这个问题。imho:)当然,我知道你问了什么,我回答了什么,我没有要求赞成票,我只是困惑为什么我的答案没有帮助,而明显地说,DI的缺点是它太酷,帮助了很多。
在过去的6个月里,我一直在使用Guice(Java DI框架)。总的来说,我认为它很好(特别是从测试的角度),但也有一些缺点。最值得注意的是:
- 代码可能变得难以理解。依赖注入可用于非常…创意…方法。例如,我刚刚遇到一些代码使用自定义注释来注入特定的iostream(例如:@server1stream,@server2stream)。虽然这确实有效,而且我承认它有一定的优雅,但它使理解guice注入成为理解代码的先决条件。
- 学习项目时有较高的学习曲线。这与第1点有关。为了理解使用依赖注入的项目如何工作,您需要了解依赖注入模式和特定框架。当我开始我目前的工作时,我花了很多困惑的时间在幕后摸索基斯在做什么。
- 建设者变大了。尽管这在很大程度上可以通过默认的构造函数或工厂来解决。
- 错误可以被混淆。我最近的一个例子是我在2个标志名上发生了冲突。Guice默默地接受了错误,我的一个标志没有初始化。
- 将错误推送到运行时。如果您的guice模块配置不正确(循环引用、错误绑定等),那么大多数错误在编译期间都不会被发现。相反,当程序实际运行时,错误会暴露出来。
现在我已经抱怨了。让我说,我将继续(自愿地)在我当前的项目中使用Guice,很可能是我的下一个项目。依赖注入是一种非常强大的模式。但它肯定会令人困惑,而且您几乎肯定会花一些时间诅咒您选择的任何依赖注入框架。
另外,我同意其他海报,依赖注入可以被过度使用。
- 我不明白为什么人们不为我做更多的"错误被推到运行时"。这是一个交易破坏者,静态输入和编译时错误是给开发人员的最大礼物,我不会为了任何东西而丢弃它们。
- @Richardtingle,IMO在应用程序启动过程中,DI模块将首先初始化,因此一旦应用程序启动,模块中的任何错误配置都将立即可见,而不是几天后或几天后。也可以增量加载模块,但是如果我们坚持DI的精神,在初始化应用程序逻辑之前限制模块加载,我们可以成功地隔离到应用程序开始的坏绑定。但是,如果我们将其配置为服务定位器反模式,那么那些坏的绑定肯定会令人吃惊。
- @Richardtingle我理解它永远不会类似于编译器提供的安全网,但是DI作为一个正确使用的工具,如框中所述,那么这些运行时错误仅限于应用程序初始化。然后我们可以将应用程序初始化视为DI模块的一种编译阶段。根据我的经验,大多数情况下,如果应用程序启动,那么其中不会有坏的绑定或不正确的引用。PS-我用过ninject和c#
- @Richardtingle——我同意你的观点,但是为了获得松散耦合的、可测试的代码,这是一种权衡。正如K4VIN所说,在初始化时发现缺少依赖项,使用接口仍然有助于编译时错误。
- 我会在您的列表中添加"难以保持代码整洁"。在广泛测试没有注册的代码之前,您永远不知道是否可以删除注册。
没有任何DI的代码会有陷入意面代码的风险——一些症状是类和方法太大、做得太多并且不容易更改、分解、重构或测试。
使用DI的代码可以是饺子代码,其中每个小类就像一个单独的饺子块-它做一件小事,并坚持单一的责任原则,这是好的。但是,单独看一个类,很难看到系统作为一个整体在做什么,因为这取决于所有这些小部分是如何组合在一起的,这是很难看到的。它看起来就像一堆小东西。
通过避免大类中大量耦合代码的意大利面复杂性,您将面临另一种复杂性的风险,即有许多简单的小类,它们之间的交互非常复杂。
我不认为这是一个致命的缺点-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实例作为依赖项。这将改变您的依赖对象的构造函数,使您更加了解实现细节,例如对象构造开销,这也是不可取的。
- 我理解您所说的,但我认为"当您创建依赖项的实现时,您必须考虑到DI来设计它"是不公平的,我认为更准确的说法是"当与使用有关实例化成本和缺少副作用或失败的最佳实践来实现的类一起使用时,DI最有效。施工期间的模式"。最糟糕的情况是,您总是可以注入一个延迟的代理实现,它将实际对象的分配推迟到第一次使用。
- 任何现代的IOC容器都允许您为特定的抽象类型/接口(总是唯一的、单例的、HTTP作用域的等)指定对象的生存期。您还可以提供一个工厂方法/委托,使用func或lazy惰性地实例化它。
- 当场。不必要地实例化依赖项会占用内存。我通常使用带有getter的"惰性加载",getter在调用时只实例化一个类。可以提供不带参数的默认构造函数,以及接受实例化依赖项的后续构造函数。例如,在第一种情况下,当try/catch语句中发生错误时,仅使用this.errorLogger.WriteError(ex)实例化实现IErrorLogger的类。
这更像是吹毛求疵。但是依赖注入的一个缺点是它使开发工具更难理解和导航代码。
具体来说,如果您在代码中控制click/command单击一个方法调用,它将把您带到接口上的方法声明,而不是具体的实现。
这实际上是松散耦合代码(由接口设计的代码)的一个缺点,即使您不使用依赖注入(即,即使您只是使用工厂),也适用。但是依赖注入的出现确实鼓励了松散耦合的代码向大众开放,所以我想我会提到它。
另外,松耦合代码的好处远远超过了这一点,因此我称之为吹毛求疵。尽管我已经工作了足够长的时间,知道如果您尝试引入依赖注入,这可能是一种推回。
实际上,我冒昧地猜测,对于依赖注入所能找到的每一个"缺点",您都会发现许多好处远远超过它。
- 带resharper的ctrl-shift-b将带您实现
- 但是我们(在一个理想的世界中)是否会对每件事情都有至少2个实现,即至少一个真正的实现和一个用于单元测试的模拟实现;-)
- 在Eclipse中,如果您按住ctrl并将鼠标悬停在方法调用代码上,它将向您显示一个菜单,以打开声明或实现。
- 如果您在接口定义中,突出显示方法名,并按ctrl+t键调出该方法的类型层次结构——您将在类型层次结构树中看到该方法的每个实现者。
基于构造函数的依赖注入(不借助于神奇的"框架")是构建OO代码的一种干净而有益的方法。在我所见过的最好的代码库中,多年来,我和马丁·福勒的其他前同事一起,开始注意到大多数这样编写的好类最终都有一个单一的doSomething方法。
那么,主要的缺点是,一旦你意识到这仅仅是一种笨拙的将闭包作为类来编写的OO方法,为了获得函数式编程的好处,你编写OO代码的动机很快就会消失。
- 基于构造函数的问题是,您总是需要添加更多的构造函数参数…
- 哈哈。如果你做的太多,你很快就会发现你的代码很糟糕,你会重构它。另外,ctrl-f6也不难。
- 当然,反过来也是正确的:这只是一种笨拙的长时间编写闭包类的函数方法,以获得OO编程的好处。
- 很多年前,我用Perl中的闭包构建了一个对象系统,所以我明白了你的意思,但是封装数据并不是OO编程的一个独特优势,所以还不清楚OO的这些有益特性是什么,在函数语言中,这些特性相当笨拙,而且需要很长时间才能获得。
我发现构造器注入会导致非常难看的构造器,(并且我在代码库中使用它——也许我的对象太细粒度了?).此外,有时在构造函数注入中,我会得到可怕的循环依赖(尽管这很少见),因此您可能会发现自己必须在更复杂的系统中拥有某种准备就绪的状态生命周期和多轮依赖注入。
但是,我更喜欢construtor注入而不是setter注入,因为一旦构建了对象,我就毫无疑问地知道它处于什么状态,是在单元测试环境中,还是装载在某个ioc容器中。它,以一种迂回的方式,是说,我认为是塞特注射的主要缺点。
(作为旁注,我确实觉得整个主题非常"宗教化",但您的里程数将随着开发团队的技术热情水平而变化!)
- 如果您有大而丑的构造函数,可能是您的类太大了,而您只需要很多依赖性?
- 这当然是我愿意招待的一种可能性!…可惜的是,我没有一个团队可以和我一起做同行评审。小提琴在背景中轻柔地演奏,然后屏幕逐渐变黑…
- 正如马克·西曼在"这对你的事业可能是危险的"上面所悲伤的那样…
- 我根本不知道背景叙述在这里能如此富有表现力:p
- @罗伯特,我在找引言,他在上面说的什么地方?
- @罗伯特,一个服务可以是一个容器类,所以是的,它可以很大,但是它是实现的正确方法。如果这是一个必须处理很多事情的服务,那么是的,您必须在构造函数中放置很多依赖项。这只是生活的一个事实——)
如果您使用的是不带IOC容器的DI,那么最大的缺点就是您很快就会看到代码实际上有多少依赖项,以及它们之间的耦合有多紧密。(但我认为这是一个好的设计!)自然的进展是朝着一个IOC容器前进,它需要一点时间来学习和实现(并没有WPF学习曲线那么糟糕,但也不是免费的)。最后一个缺点是,一些开发人员将开始编写诚实可靠的单元测试,这将需要他们一些时间来解决。以前可以在半天内完成某件事情的开发人员会突然花费两天时间来尝试如何模拟他们所有的依赖关系。
与MarkSeemann的回答类似,底线是你花时间成为一个更好的开发人员,而不是把代码的一部分拼凑在一起,然后把它扔出去/投入生产。你的生意更喜欢哪一个?只有你能回答。
- 好点。那就是质量差/交货快与质量好/交货慢。不利的一面?DI不是魔术,期望它是缺点。
DI是一种技术或模式,与任何框架都不相关。您可以手动连接依赖项。DI帮助您处理SR(单一责任)和SOC(分离关注点)。DI带来了更好的设计。从我的观点和经验来看,没有什么缺点。像其他模式一样,您可能会弄错或误用它(但DI的情况非常困难)。
如果您将DI作为原则引入到遗留应用程序中,那么使用一个框架——您能做的最大错误就是将其误用为服务定位器。DI+框架本身很好,在我看到的任何地方都能让事情变得更好!从组织的角度来看,每一个新的过程、技术、模式……都存在着共同的问题:
- 你必须训练你的球队
- 您必须更改您的申请(包括风险)
一般来说,你必须投入时间和金钱,除此之外,没有什么坏处,真的!
代码可读性。由于依赖项隐藏在XML文件中,所以您将无法轻松地找出代码流。
- 您不需要使用XML配置文件进行依赖项注入—选择一个在代码中支持配置的DI容器。
- 依赖注入并不一定意味着XML文件。在Java中似乎仍然如此,但在.NET中,我们几年前就放弃了这种耦合。
- 或者根本不使用DI容器。
- 如果您知道代码使用的是DI,那么您可以很容易地假设谁设置了依赖项。
- 例如,在爪哇,谷歌的Guice不需要XML文件。
两件事:
例如,Intellij(商业版)支持检查Spring配置的有效性,并将标记错误,如配置中的类型冲突。如果没有这种工具支持,就无法在运行测试之前检查配置是否有效。
这就是为什么"cake"模式(scala社区都知道)是一个好主意的原因之一:可以通过类型检查器检查组件之间的连接。注释或XML并没有这样的好处。
像Spring或Guice这样的框架使静态地确定由容器创建的对象图是什么样子变得困难。尽管它们在容器启动时创建了一个对象图,但它们不提供描述已创建/将要创建的对象图的有用API。
- 这绝对是扯淡。依赖注入是一个概念,它不需要fricken框架。你所需要的只是新的。抛开Spring和所有这些废话,那么您的工具将工作得很好,而且您将能够更好地重构代码。
- 是的,很有道理。我应该更清楚地说,我所说的是DI框架的问题,而不是模式。在我回答问题时,我可能错过了对问题的澄清(假设当时它就在那里)。
- 啊。在这种情况下,我高兴地收回我的烦恼。道歉。
当您不断地使用技术来处理静态类型时,静态类型语言的假定好处似乎会显著减少。我刚刚采访过的一个大型Java商店是用静态代码分析来映射他们的构建依赖关系……它必须解析所有的Spring文件才能有效。
它可以增加应用程序的启动时间,因为IOC容器应该以适当的方式解决依赖关系,有时需要进行多次迭代。
- 为了给出一个数字,DI容器应该每秒解析数千个依赖项。(codingstinct.com/2008/05/&hellip;)DI容器允许延迟实例化。性能(即使在大型应用程序中)也不成问题。或者至少性能问题应该是可以解决的,而不是反对国际奥委会及其框架的理由。