我正在尝试将DI作为一种模式在工作中引入,我们的一位主要开发人员想知道:使用依赖注入模式的缺点是什么 - 如果有的话?
注意我在这里寻找 - 如果可能 - 详尽的清单,而不是对该主题的主观讨论。
澄清:我说的是依赖注入模式(参见Martin Fowler的this article),不是特定的框架,无论是基于XML(如Spring)还是基于代码(如Guice),还是“自动滚动”。
编辑:在此/r/programming进行了一些重要的进一步讨论/咆哮/辩论。
答案 0 :(得分:200)
有几点:
通常,解耦的好处使每个任务更易于阅读和理解,但会增加编排更复杂任务的复杂性。
答案 1 :(得分:178)
与面向对象编程,样式规则以及其他所有内容相关的基本问题。事实上,这可能是非常普遍的 - 做太多的抽象,增加太多的间接性,并且通常在错误的地方过度使用好的技术。
您应用的每个模式或其他构造都会带来复杂性。抽象和间接散布信息,有时会将不相关的细节移开,但同样有时会更难理解究竟发生了什么。您应用的每条规则都会带来不灵活性,排除可能只是最佳方法的选项。
重点是编写能够完成工作并且健壮,可读和可维护的代码。您是软件开发人员 - 而不是象牙塔建设者。
相关链接
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
这些“依赖关系”不仅仅是原始开发人员的问题 - 使用该界面的每个人都必须查找依赖关系是什么,如何指定它们以及它们的含义,并找出为他们做什么应用。这就是“合理默认”这个词可以让生活变得更加简单。
面向对象的依赖注入原则上没有区别。在源代码文本和开发人员时间编写类都是一种开销,如果根据某些依赖对象规范编写该类来提供依赖关系,则依赖对象被锁定为支持该接口,即使有需要也是如此。替换该对象的实现。
这一切都不应该被解读为声称依赖注入是坏的 - 远非如此。但是任何好的技术都可以过度应用并且在错误的地方。正如不是每个字符串都需要被提取出来并转化为参数一样,并不是每个低级行为都需要从高级对象中提取出来并变成可注入的依赖项。
答案 2 :(得分:75)
这是我自己的初步反应:基本上与任何模式相同的缺点。
答案 3 :(得分:45)
控制反转的最大“缺点”(不是DI,但足够接近)是它倾向于删除只有一个点来查看算法的概述。这基本上就是当你有解耦代码时会发生的事情 - 在一个地方看的能力是一种紧密耦合的神器。
答案 4 :(得分:41)
我不认为这样的列表存在,但是尝试阅读这些文章:
答案 5 :(得分:39)
在过去的6个月里,我一直在广泛使用Guice(Java DI框架)。总的来说,我觉得它很棒(特别是从测试的角度来看),但也有一些缺点。最值得注意的是:
现在我已经抱怨了。让我说我会继续(心甘情愿地)在我目前的项目中使用Guice,而且很可能是我的下一个。依赖注入是一种伟大且极其强大的模式。但它肯定会令人困惑,你几乎肯定会花一些时间诅咒你选择的任何依赖注入框架。
此外,我同意其他海报,依赖注入可能被滥用。
答案 6 :(得分:24)
没有任何DI的代码会遇到众所周知的混淆Spaghetti code的风险 - 一些症状是类和方法太大,做得太多而且不容易改变,分解,重构,或测试。
使用DI的代码很多可以是Ravioli code,其中每个小类就像一个单独的馄饨块 - 它做了一件小事并且single responsibility principle被遵守,这很好。但是看着自己的课程很难看出整个系统的作用,因为这取决于所有这些小部件是如何组合在一起的,这很难看出来。它看起来像一大堆小东西。
通过避免大类中大量耦合代码的意大利面复杂性,你冒着另一种复杂性的风险,那里有很多简单的小类,它们之间的交互很复杂。
我不认为这是一个致命的缺点 - DI仍然非常值得。一定程度的馄饨风格与小班只做一件事可能是好的。即使过量,我认为它不像意大利面条代码那么糟糕。但是要意识到它可以走得太远是避免它的第一步。请点击链接,了解如何避免它。
答案 7 :(得分:13)
如果您有自己开发的解决方案,那么依赖关系就在构造函数中。或者也许作为方法参数再次不难发现。虽然框架管理的依赖关系,如果走极端,可能会开始像魔术一样。
然而,在太多类中有太多依赖关系是一个明显的迹象,表明你的类结构被搞砸了。因此,在某种程度上,依赖注入(本土或框架管理)可以帮助带来明显的设计问题,否则可能隐藏在黑暗中。
为了更好地说明第二点,这里是article(original source)的摘录,我完全相信这是构建任何系统的基本问题,而不仅仅是计算机系统。
假设你想设计一个大学校园。你必须将一些设计委托给学生和教授,否则物理学建筑将不适合物理学家。没有一个建筑师能够充分了解物理学家需要做什么。但你不能将每个房间的设计委托给它的居住者,因为那样你就会得到一堆巨大的碎石。
如何在大型层次结构的各个层面分配设计责任,同时仍保持整体设计的一致性和和谐性?这是亚历山大试图解决的架构设计问题,但它也是计算机系统开发的基本问题。
DI能解决这个问题吗? 否即可。但是,如果您试图将设计每个房间的责任委托给其居住者,它确实可以帮助您清楚地看到。
答案 8 :(得分:12)
有一件事让我对DI产生了一点点假设是假设所有注入的对象便宜实例化并且不会产生副作用 -OR-使用依赖关系经常超过任何相关的实例化成本。
如果在消费类中使用依赖经常,那么这可能很重要;比如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>
实例作为依赖项。这将改变你的依赖对象的构造函数,然后更加认识到实现细节,例如对象构造开销,这也可能是不可取的。
答案 9 :(得分:12)
您只是通过实现依赖注入而无需将其解耦而将代码解耦的错觉。我认为这是DI最危险的事情。
答案 10 :(得分:11)
这更像是一种挑剔。但依赖注入的一个缺点是它使开发工具更难以推理和导航代码。
具体来说,如果您在代码中按Control-Click / Command-Click方法调用,它将带您进入接口上的方法声明而不是具体实现。
这实际上是松散耦合代码(由接口设计的代码)的缺点,即使您不使用依赖注入(即使您只是使用工厂)也适用。但是,依赖注入的出现真正鼓励松散耦合的代码到群众,所以我想我会提到它。
此外,松耦合代码的好处远远超过这个,因此我称之为挑剔。虽然我已经工作了很长时间才知道如果你试图引入依赖注入,这可能会有所回击。
事实上,我冒昧地猜测,你可以找到依赖注入的每一个“缺点”,你会发现许多优势远远超过它。
答案 11 :(得分:10)
基于构造函数的依赖注入(没有神奇的“框架”的帮助)是构建OO代码的一种干净且有益的方式。在我见过的最好的代码库中,多年来与Martin Fowler的其他前同事一起度过,我开始注意到这样编写的大多数优秀的类最终只有一个doSomething
方法。
然后,主要的缺点是,一旦你意识到它只是一个长期的OO编写闭包的方式,为了获得函数式编程的好处,你编写OO代码的动机很快就会消失。 / p>
答案 12 :(得分:9)
我发现构造函数注入会导致很难看的构造函数,(我在整个代码库中使用它 - 也许我的对象太精细了?)。此外,有时使用构造函数注入时,我最终会遇到可怕的循环依赖(尽管这种情况非常罕见),因此您可能会发现自己必须在更复杂的系统中进行多轮依赖注入的状态生命周期。
然而,我赞成construtor注入而不是setter注入,因为一旦我的对象被构造,那么我毫无疑问地知道它处于什么状态,无论是在单元测试环境中还是在某个IOC容器中加载。其中,以一种迂回的方式,说出我认为塞特尔注射的主要缺点。
(作为旁注,我确实发现整个主题非常“宗教”,但你的里程会因你的开发团队的技术狂热程度而有所不同!)
答案 13 :(得分:8)
如果您在没有IOC容器的情况下使用DI,最大的缺点是您可以快速了解代码实际拥有的依赖关系以及所有内容的紧密耦合程度。 (“但我认为这是一个很好的设计!”)自然的进展是向IOC容器迈进,这需要花费一点时间来学习和实现(不像WPF学习曲线那么糟糕,但它不是免费的其一)。最后的缺点是一些开发人员将开始写好诚实的单元测试,它将花费时间来弄明白。以前可以在半天内解决问题的开发人员会突然花两天时间试图弄清楚如何模拟他们所有的依赖关系。
类似于Mark Seemann的回答,最重要的是你花时间成为一个更好的开发人员,而不是将一些代码混杂起来并将其抛到门外/投入生产。你的企业会选择哪个?只有你能回答这个问题。
答案 14 :(得分:5)
DI是一种技术或模式,与任何框架无关。您可以手动连接依赖项。 DI帮助您使用SR(单一责任)和SoC(关注点分离)。 DI导致更好的设计。从我的观点和经验没有缺点。与任何其他模式一样,你可能会错误或误用它(但在DI的情况下相当困难)。
如果您将DI作为原则引入遗留应用程序,使用框架 - 您可以做的最大的错误就是将其误用为服务定位器。 DI + Framework本身很棒,只要我看到它,就会让事情变得更好!从组织的角度来看,每个新流程,技术,模式都有共同的问题......:
一般来说,你必须投入时间和金钱,除此之外,真的没有缺点!
答案 15 :(得分:3)
代码可读性。您将无法轻松找出代码流,因为依赖项隐藏在XML文件中。
答案 16 :(得分:2)
当您不断使用技术来解决静态类型时,似乎静态类型语言的假设好处显着减少。我刚刚采访过的一家大型Java商店正在使用静态代码分析来构建它们的构建依赖关系...它必须解析所有Spring文件才能生效。
答案 17 :(得分:2)
两件事:
例如,IntelliJ(商业版)支持检查Spring配置的有效性,并会在配置中标记类型违规等错误。如果没有这种工具支持,在运行测试之前无法检查配置是否有效。
这就是为什么'蛋糕'模式(因为Scala社区已知)是一个好主意的原因之一:组件之间的连线可以通过类型检查器进行检查。使用注释或XML没有这种好处。
像Spring或Guice这样的框架使得很难确定容器创建的对象图形是什么样的。虽然它们在容器启动时创建了一个对象图,但是它们没有提供描述/将要创建的对象图的有用API。
答案 18 :(得分:1)
它可以增加应用启动时间,因为IoC容器应该以适当的方式解析依赖关系,有时需要进行多次迭代。