我正在尝试在这里将DI作为一种模式进行介绍,我们的一位主要开发人员想知道:使用依赖注入模式的缺点 -如果有的话-有什么缺点?
请注意,我正在这里寻找(如果可能)详尽列表,而不是对该主题进行主观讨论。
澄清:我正在谈论的是依赖注入模式(请参阅Martin Fowler的本文),而不是特定的框架,无论是基于XML的(例如Spring)还是基于代码的(例如Guice),还是“自卷式” 。
编辑:在这里/ r / programming进行了一些很棒的进一步讨论/讨论/辩论。
我正在尝试在这里将DI作为一种模式进行介绍,我们的一位主要开发人员想知道:使用依赖注入模式的缺点 -如果有的话-有什么缺点?
请注意,我正在这里寻找(如果可能)详尽列表,而不是对该主题进行主观讨论。
澄清:我正在谈论的是依赖注入模式(请参阅Martin Fowler的本文),而不是特定的框架,无论是基于XML的(例如Spring)还是基于代码的(例如Guice),还是“自卷式” 。
编辑:在这里/ r / programming进行了一些很棒的进一步讨论/讨论/辩论。
Answers:
要点:
通常,去耦的好处使每个任务更易于阅读和理解,但是却增加了编排更复杂任务的复杂性。
与面向对象的编程,样式规则以及几乎所有其他内容经常遇到的相同基本问题。实际上,很可能会做太多的抽象工作,增加太多的间接访问,并且可能过度地在错误的地方应用良好的技术。
您应用的每种模式或其他构造都会带来复杂性。抽象和间接分散信息,有时将无关紧要的细节移开,但有时也使得更难于准确了解正在发生的事情。您应用的每条规则都会带来灵活性,排除可能只是最佳方法的选项。
关键是编写可以完成工作并且健壮,易读和可维护的代码。您是软件开发人员,而不是象牙塔的构建者。
相关连结
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/zh-cn/library/ms632680%28v=vs.85%29.aspx
那是要处理的12个“依赖项”。例如,如果屏幕分辨率变得非常高,也许我们将需要64位坐标值-以及另一个版本的CreateWindowEx。是的,已经有一个较旧的版本徘徊,大概是在幕后映射到了较新的版本...
http://msdn.microsoft.com/zh-cn/library/ms632679%28v=vs.85%29.aspx
对于原始开发人员而言,这些“依赖关系”不只是一个问题-使用该接口的每个人都必须查找依赖关系是什么,如何指定依赖关系以及它们的含义,并弄清楚如何为应用程序做些什么。在这里,“明智的违约”可以使生活变得更加简单。
面向对象的依赖注入在原理上没有什么不同。无论是在源代码文本中还是在开发人员时,编写类都是一项开销,并且如果根据某些相关对象规范编写该类以提供相关性,则即使有需要,该相关对象也被锁定为支持该接口。替换该对象的实现。
所有这些都不应该被认为是依赖注入是不好的-远非如此。但是,任何好的技术都可能在错误的地方过度使用。正如不是每个字符串都需要提取出来并转化为参数一样,并不是每个低级行为都需要从高级对象中提取出来并转化为可注入的依赖关系。
这是我自己的初步反应:任何模式基本上都有相同的缺点。
控制反转的最大“缺点”(不是完全的DI,但足够接近)是它倾向于消除只看算法概述的问题。不过,这基本上就是在将代码解耦后会发生的事情-能够在一个地方查看是紧密耦合的产物。
我认为这样的列表不存在,但是请尝试阅读这些文章:
在过去的6个月中,我一直在广泛使用Guice(Java DI框架)。总体而言,我认为它很棒(尤其是从测试的角度来看),但存在某些缺点。最为显着地:
现在,我已经抱怨了。我要说的是,我将继续(愿意)在当前项目中(很可能在下一个项目中)使用Guice。依赖注入是一种非常强大的模式。但这肯定会造成混乱,并且几乎可以肯定,您将花费一些时间来诅咒您选择的任何依赖注入框架。
另外,我同意其他张贴者的观点,即依赖注入可能会被过度使用。
没有任何DI的代码会陷入陷入Spaghetti代码的众所周知的风险-有些症状是类和方法太大,做得太多并且不容易更改,分解,重构或测试。
经常使用DI的代码可以是Ravioli代码,其中每个小类都像一个单独的馄饨块-它只做一件小事情,并且遵循单一职责原则,这很好。但是,单独查看类很难看到系统整体的功能,因为这取决于所有这些小部分如何组合在一起,这很难看到。看起来就像一大堆小东西。
通过避免大型类中大量耦合代码的意大利面条式复杂性,您将冒另一种复杂性的风险,其中存在许多简单的小类,并且它们之间的交互非常复杂。
我认为这不是致命的缺点-DI仍然非常值得。某种程度的馄饨风格和仅做一件事的小类可能是好的。即使过多,我也认为这不是意大利面条代码的坏处。但是要意识到它可能采取的措施过于严格,这是避免它的第一步。请点击链接以讨论如何避免这种情况。
如果您有自己开发的解决方案,则在构造函数中,依赖关系就在您的面前。或作为方法参数,也不太难发现。虽然框架管理的依赖项,如果放到了极点,可能会开始看起来像魔术。
但是,在太多的类中有太多的依赖关系是一个清晰的信号,表明您的类结构已经搞砸了。因此,以某种方式进行依赖项注入(本地或框架管理)可以帮助解决明显的设计问题,否则这些问题可能会被隐藏在黑暗中。
为了更好地说明第二点,这是本文的摘录(原始资料),我全心全意地相信这是构建任何系统而不仅仅是计算机系统的根本问题。
假设您要设计一个大学校园。您必须将某些设计委托给学生和教授,否则物理大楼对物理人员来说将无法正常工作。没有任何一位建筑师对物理学家需要自己做的事情了解得足够多。但是您不能将每个房间的设计委托给它的占用者,因为那样您会得到一堆巨大的瓦砾。
您如何在整个层次结构的所有层次上分配设计职责,同时又保持整体设计的一致性和协调性?这是Alexander试图解决的体系结构设计问题,但这也是计算机系统开发的基本问题。
DI解决了这个问题吗?没有。但这确实有助于您清楚地知道是否要将设计每个房间的责任委托给它的居住者。
有一两件事让我不安与DI一点是假设所有注入的对象是便宜的实例化和不产生副作用的依赖,使用非常频繁,它胜过任何相关的实例化成本-或- 。
当消费类中不经常使用依赖项时,这可能很重要。例如诸如此类的东西IExceptionLogHandlerService
。显然,此类服务很少在类中被调用(希望:))-大概仅在需要记录的异常时才调用。然而,规范的构造喷射模式 ...
Public Class MyClass
Private ReadOnly mExLogHandlerService As IExceptionLogHandlerService
Public Sub New(exLogHandlerService As IExceptionLogHandlerService)
Me.mExLogHandlerService = exLogHandlerService
End Sub
...
End Class
...要求提供此服务的“实时”实例,以破坏实现该服务所需的成本/副作用。不太可能,但是如果构造此依赖项实例涉及服务/数据库命中,配置文件查找或锁定资源直到处置该怎么办?如果此服务是按需,服务定位或工厂生成的(都是有问题的)构建的,那么您仅在必要时承担构建成本。
现在,一种公认的软件设计原则是,构造一个对象很便宜并且不会产生副作用。尽管这是一个很好的概念,但并非总是如此。但是,使用典型的构造函数注入基本上要求是这种情况。意味着在创建依赖项的实现时,必须在设计时考虑到DI。也许您本来可以使对象构造的成本更高,从而在其他地方获得收益,但是如果要注入此实现,则可能会迫使您重新考虑该设计。
顺便说一句,某些技术可以通过允许延迟注入已注入的依赖项来缓解此确切问题,例如为类提供Lazy<IService>
实例作为依赖项。这将改变您的从属对象的构造函数,从而使您更加意识到实现细节,例如对象构造费用,这当然也是不希望的。
this.errorLogger.WriteError(ex)
在try / catch语句中发生错误时才实例化实现IErrorLogger的类。
您只是通过实现依赖注入而没有实际将其解耦来使代码解耦的错觉。我认为这是DI最危险的事情。
这更像是挑剔。但是依赖注入的缺点之一是,这使开发工具难以推理和导航代码。
具体来说,如果您按住Control键单击/ Command键单击代码中的方法调用,它将带您进入接口上的方法声明,而不是具体的实现。
这实际上是松散耦合代码(由接口设计的代码)的缺点,并且即使您不使用依赖项注入(即,即使您仅使用工厂)也适用。但是依赖注入的出现才真正促使了将松散耦合的代码带给大众,因此我想我会提到它。
另外,松耦合代码的好处远不止于此,因此我将其称为nitpick。尽管我已经工作了很长时间,但是如果尝试引入依赖项注入,您可能会遇到这种回退。
实际上,我敢冒险猜测,对于依赖注入,您可以找到的每一个“缺点”,您都会发现许多远远超过它的优点。
基于构造函数的依赖注入(无需借助神奇的“框架”)是一种构造OO代码的干净而有益的方法。在我所见过的最好的代码库中,与Martin Fowler的其他前同事一起工作了数年之后,我开始注意到以这种方式编写的大多数好的类最终都只有一个doSomething
方法。
因此,主要的缺点是,一旦您意识到这仅仅是一种将类作为对象编写闭包的笨拙的长途OO方法,以便获得功能编程的好处,那么编写OO代码的动力就会迅速消失。
我发现构造函数注入会导致庞大的丑陋构造函数,(并且在整个代码库中都使用它-也许我的对象太细粒度了?)。另外,有时使用构造函数注入会导致可怕的循环依赖关系(尽管这种情况非常罕见),因此您可能会发现自己必须拥有某种就绪状态生命周期,并且在更复杂的系统中需要进行几轮依赖关系注入。
但是,相对于setter注入,我更喜欢使用construtor注入,因为一旦构造了我的对象,那么我无疑会知道它处于什么状态,无论是在单元测试环境中还是在某些IOC容器中加载。这以一种about回的方式在说,我认为是二传手注射的主要缺点。
(作为一个旁注,我确实发现整个主题都非常“虔诚”,但是您的努力程度会随开发团队中技术狂热程度的变化而变化!)
如果您使用的是没有IOC容器的DI,那么最大的缺点就是您会很快看到您的代码实际上具有多少依赖关系以及所有东西之间的紧密联系。(“但是我认为这是一个很好的设计!”)自然的过程是朝着IOC容器迈进,这需要花费一些时间来学习和实施(虽然不如WPF学习曲线那么糟糕,但是它不是免费的)要么)。最后的缺点是一些开发人员将开始编写诚实到善良的单元测试,这将需要他们花费一些时间来解决。以前可以在半天之内完成某些工作的开发人员会突然花两天的时间来弄清楚如何模拟所有依赖项。
类似于Mark Seemann的回答,最重要的是,您花时间成为一名更好的开发人员,而不是将一些代码一起乱搞然后扔出去/投入生产。您的公司宁愿拥有哪一家?只有你能回答。
DI是一种技术或模式,与任何框架都不相关。您可以手动连接依赖项。DI可帮助您实现SR(单一职责)和SoC(关注点分离)。DI导致更好的设计。从我的观点和经验来看,没有缺点。像使用其他任何模式一样,您可能会出错或滥用它(但是在DI的情况下很难)。
如果您使用框架将DI作为原则引入到旧版应用程序中,那么您最大的错误就是将其误用作服务定位器。DI + Framework本身很棒,并且在我看到的任何地方都能使它变得更好!从组织的角度来看,每个新流程,新技术,新模式都存在一些共同的问题:
一般来说,您必须投入时间和金钱,除此之外,真的没有缺点!
代码可读性。由于依赖关系隐藏在XML文件中,因此您将无法轻松弄清楚代码流。
两件事情:
例如,IntelliJ(商业版)支持检查Spring配置的有效性,并将标记错误,例如配置中的类型冲突。没有这种工具的支持,您将无法在运行测试之前检查配置是否有效。
这就是“蛋糕”模式(Scala社区众所周知)是一个好主意的原因之一:组件之间的接线可以通过类型检查器进行检查。注释或XML并没有带来任何好处。
诸如Spring或Guice之类的框架使得难以静态确定容器创建的对象图的外观。尽管它们在启动容器时创建了一个对象图,但是它们没有提供描述/ would /创建的对象图的有用API。
由于IoC容器应以适当的方式解决依赖关系,因此有时可能需要进行多次迭代,因此可能会增加应用程序的启动时间。