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