使用依赖注入有什么缺点? [关闭]

我正在尝试将DI作为一种模式在这里工作,我们的一位主要开发人员想知道:什么 - 如果有的话 - 是 downsides 使用依赖注入模式?

注意我在这里寻找 - 如果可能 - 详尽的清单,而不是关于该主题的主观讨论 .


Clarification :我在谈论依赖注入模式(参见Martin Fowler的this article),而不是特定的框架,无论是基于XML(如Spring)还是基于代码(如Guice),还是"self-rolled" .


Edit :这里有一些很好的进一步讨论/咆哮/辩论 .

回答(19)

3 years ago

几点:

  • DI通常通过增加类的数量来增加复杂性,因为职责分离得更多,这并不总是有益的

  • 您的代码将(某种程度上)耦合到您使用的依赖注入框架(或者更一般地说,您决定如何实现DI模式)

  • 执行类型解析的DI容器或方法通常会产生轻微的运行时间损失(非常微不足道,但它存在)

通常,解耦的好处使每个任务更易于阅读和理解,但增加了编排更复杂任务的复杂性 .

3 years ago

使用面向对象编程,样式规则以及其他所有内容时,您经常会遇到同样的基本问题 . 事实上,这可能是非常普遍的 - 做太多的抽象,增加太多的间接性,并且通常在错误的地方过度使用好的技术 .

您应用的每个模式或其他构造都会带来复杂性抽象和间接散布信息,有时会将不相关的细节移开,但同样有时会更难理解究竟发生了什么 . 您应用的每条规则都会带来不灵活性,排除可能只是最佳方法的选项 .

重点是编写能够完成工作并且健壮,可读和可维护的代码 . 您是软件开发人员 - 而不是象牙塔建设者 .

Relevant Links

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

这些“依赖关系”不仅仅是原始开发人员的问题 - 使用该接口的每个人都必须查找依赖关系是什么,如何指定它们以及它们的含义,并找出应对其应用程序的操作 . 这就是“合理默认”这个词可以让生活变得更加简单 .

面向对象的依赖注入原则上没有区别 . 在源代码文本和开发人员时间编写类都是一种开销,如果根据某些依赖对象规范编写该类来提供依赖关系,则依赖对象被锁定为支持该接口,即使有需要也是如此 . 替换该对象的实现 .

这些都不应该被解读为声称依赖注入是坏的 - 远非如此 . 但是任何好的技术都可以过度地应用在错误的地方 . 正如不需要提取每个字符串并将其转换为参数一样,并非每个低级行为都需要从高级对象中提取出来并转换为可注入的依赖项 .

3 years ago

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

  • 需要时间学习

  • 如果被误解可以导致弊大于利

  • 如果采取极端措施,可能比证明利益更合理

3 years ago

控制反转的最大“缺点”(不是DI,但足够接近)是它倾向于删除只有一个点来查看算法的概述 . 这基本上是当你有解耦代码时会发生的事情 - 在一个地方寻找的能力是一种紧密耦合的神器 .

3 years ago

在过去的6个月里,我一直在广泛使用Guice(Java DI框架) . 总的来说,我觉得它很棒(特别是从测试的角度来看),但也有一些缺点 . 最为显着地:

  • Code can become harder to understand. 依赖注入可以非常......创造性的方式使用 . 例如,我遇到了一些使用自定义注释来注入某些IOStream的代码(例如:@ Server1Stream,@ Server2Stream) . 虽然这确实有效,但我承认它有一定的优雅,它使理解Guice注射成为理解代码的先决条件 .

  • Higher learning curve when learning project. 这与第1点有关 . 为了理解使用依赖注入的项目如何工作,您需要了解依赖注入模式和特定框架 . 当我开始目前的工作时,我花了不少时间来研究Guice在幕后所做的事情 .

  • Constructors become large. 虽然这可以通过默认构造函数或工厂在很大程度上解决 .

  • Errors can be obfuscated. 我最近的一个例子就是我在两个旗帜名称上发生了碰撞 . Guice默默地吞下了这个错误,我的一个标志没有被初始化 .

  • Errors are pushed to run-time. 如果错误地配置Guice模块(循环引用,错误绑定......),大多数错误在编译期间都不会被发现 . 而是在程序实际运行时暴露错误 .

现在我已经抱怨了 . 让我说我将继续(心甘情愿地)在我目前的项目中使用Guice,很可能是我的下一个 . 依赖注入是一种伟大且极其强大的模式 . 但它肯定会令人困惑,你几乎肯定会花一些时间诅咒你选择的任何依赖注入框架 .

此外,我同意其他海报,依赖注入可能被滥用 .

3 years ago

没有任何DI的代码会出现众所周知的混淆风险 - 一些症状是类和方法太大,做得太多而且不容易改变,分解,重构或测试 .

使用DI的代码很多可以是Ravioli code,其中每个小类就像一个单独的馄饨块 - 它做了一件小事而single responsibility principle被遵守,这很好 . 但是看着他们自己的课程很难看出整个系统是做什么的,因为这取决于所有这些小部件如何组合在一起,这很难看出来 . 它看起来像一大堆小东西 .

通过避免大类中的大量耦合代码的意大利面复杂性,您冒着另一种复杂性的风险,其中存在许多简单的小类并且它们之间的交互是复杂的 .

我不认为这是一个致命的缺点 - DI仍然非常值得 . 一定程度的馄饨风格与小班只做一件事可能是好的 . 即使过量,我认为它不像意大利面条代码那么糟糕 . 但要意识到它可以走得太远是避免它的第一步 . 按照链接讨论如何避免它 .

3 years ago

如果你有一个自己开发的解决方案,那么依赖关系就在你的构造函数中 . 或者也许作为方法参数再次不难发现 . 虽然框架管理的依赖项,如果采取极端,可以开始像魔术一样 .

但是,在太多的类中有太多的依赖关系是一个明显的迹象,表明你的类结构被搞砸了 . 因此,在某种程度上,依赖注入(本土或框架管理)可以帮助带来明显的设计问题,否则可能隐藏在黑暗中潜伏 .


为了更好地说明第二点,这里有一个摘录自articleoriginal source),我完全相信这是构建任何系统的基本问题,而不仅仅是计算机系统 .

假设你想设计一个大学校园 . 你必须将一些设计委托给学生和教授,否则物理学的建筑将不适合物理学家 . 没有一个建筑师能够充分了解物理学家需要做什么 . 但是你不能将每个房间的设计委托给它的居住者,因为那样你就会得到一堆巨大的碎石 . 如何在大型层次结构的各个层面分配设计责任,同时仍保持整体设计的一致性和和谐性?这是亚历山大试图解决的建筑设计问题,但它也是一个问题计算机系统开发的基本问题 .

DI解决了这个问题吗? No . 但它确实可以帮助您清楚地了解您是否正在尝试将设计每个房间的责任委托给其居住者 .

3 years ago

这更像是一种挑剔 . 但依赖注入的一个缺点是它使开发工具更难以推理和导航代码 .

具体来说,如果您在代码中按Control-Click / Command-Click方法调用,它将转到接口上的方法声明而不是具体实现 .

这实际上是松散耦合代码(由接口设计的代码)的缺点,即使您不使用依赖注入(即使您只是使用工厂)也适用 . 但依赖注入的出现真正鼓励松散耦合的代码到群众,所以我想我会提到它 .

此外,松散耦合代码的好处远远超过这个,因此我称之为挑剔 . 虽然我已经工作了很长时间才知道如果你试图引入依赖注入,这可能会得到一种推迟 .

事实上,我冒昧地猜测,对于依赖注入你可以找到的每一个“缺点”,你会发现许多优势远远超过它 .

3 years ago

有一点让我对DI有点麻烦的是假设所有注入的对象实例化都很便宜并且不产生副作用 - 或者 - 依赖性被频繁使用以至于它超过任何相关的实例化成本 .

这可能是重要的,因为在消费类中不经常使用依赖关系;比如 IExceptionLogHandlerService . 显然,类似的服务在类中很少被调用(希望:)) - 大概只是需要记录的异常;然而规范的构造函数 - 注入模式......

Public Class MyClass
    Private ReadOnly mExLogHandlerService As IExceptionLogHandlerService

    Public Sub New(exLogHandlerService As IExceptionLogHandlerService)
        Me.mExLogHandlerService = exLogHandlerService
    End Sub

     ...
End Class

......要求提供此服务的“实时”实例,否定实现该服务所需的成本/副作用 . 不是说它可能会,但如果构建这个依赖实例涉及服务/数据库命中,或配置文件查找,或锁定资源直到被处理,该怎么办?如果这个服务是根据需要,服务定位或工厂生成(所有都有自己的问题)构建的,那么您将仅在必要时获取建设成本 .

现在,一种普遍接受的软件设计原则是构造一个对象既便宜又不会产生副作用 . 而那个's a nice notion, it isn'总是如此 . 然而,使用典型的构造函数注入基本上要求就是这种情况 . 这意味着当您创建依赖项的实现时,您必须考虑到DI而设计它 . 也许你会让对象构建在其他地方获得更多的成本变得更加昂贵,但是如果要注入这种实现,它可能会迫使你重新考虑这种设计 .

顺便说一下,某些技术可以通过允许延迟加载注入的依赖性来减轻这个确切的问题 . 提供一个类 Lazy<IService> 实例作为依赖项 . 这将改变你的依赖对象的构造函数,然后更加认识到实现细节,例如对象构造开销,这也可能是不可取的 .

3 years ago

基于构造函数的依赖注入(没有神奇的"frameworks"的帮助)是构造OO代码的一种干净且有益的方式 . 在我见过的最好的代码库中,多年来与Martin Fowler的其他前同事一起度过,我开始注意到以这种方式编写的大多数优秀类最终只有一个 doSomething 方法 .

那么,主要的缺点是,一旦你意识到这只是一种将闭包写为类的长期OO方式,以便获得函数式编程的好处,你编写OO代码的动机很快就会消失 .

3 years ago

你只是通过实现依赖注入而不实际解耦它来解耦你的代码的错觉 . 我认为这是DI最危险的事情 .

3 years ago

我发现构造函数注入可能会导致很难看的构造函数,(我在整个代码库中都使用它 - 也许我的对象太精细了?) . 此外,有时使用构造函数注入我最终会遇到可怕的循环依赖(尽管这种情况非常罕见),因此您可能会发现自己必须在更复杂的系统中进行多轮依赖注入的状态生命周期 .

但是,我赞成construtor注入而不是setter注入,因为一旦我的对象被构造,那么我毫无疑问地知道它处于什么状态,无论是在单元测试环境中还是在一些IOC容器中加载 . 其中,以一种迂回的方式说,我觉得塞特尔注射的主要缺点 .

(作为旁注,我确实发现整个主题非常“宗教”,但你的里程会因你的开发团队的技术狂热程度而有所不同!)

3 years ago

如果您在没有IOC容器的情况下使用DI,那么最大的缺点就是您快速了解您的代码实际拥有多少依赖项以及所有内容的紧密耦合程度 . (“但我认为这是一个很好的设计!”)自然的进展是向IOC容器迈进,这需要花费一点时间来学习和实现(不像WPF学习曲线那么糟糕,但它不是免费的其一) . 最后的缺点是一些开发人员将开始写好诚实的单元测试,它将花费时间来弄明白 . 以前可以在半天内解决问题的开发人员会突然花两天时间试图弄清楚如何模拟他们所有的依赖关系 .

与Mark Seemann的答案类似,最重要的是你花时间成为一个更好的开发人员,而不是将一些代码混杂起来并将其抛到门外/投入 生产环境 . 你的企业会选择哪个?只有你能回答这个问题 .

3 years ago

DI是一种技术或模式,与任何框架无关 . 您可以手动连接依赖项 . DI帮助您使用SR(单一责任)和SoC(关注点分离) . DI导致更好的设计 . 从我的角度和经验 there are no downsides . 与任何其他模式一样,您可能会错误或误用它(但在DI的情况下相当困难) .

如果您将DI作为原则引入遗留应用程序,使用框架 - 您可以做的最大的错误就是将其误用为服务定位器 . DI框架本身很棒,只要我看到它,就会让事情变得更好!从组织的角度来看,每个新流程,技术,模式都有共同的问题......:

  • 你必须训练你的团队

  • 您必须更改您的申请(包括风险)

一般来说,你必须 invest time and money ,除此之外,真的没有缺点!

3 years ago

代码可读性 . 您将无法轻松找出代码流,因为依赖项隐藏在XML文件中 .

3 years ago

两件事情:

  • 他们需要额外的工具支持来检查配置是否有效 .

例如,IntelliJ(商业版)支持检查Spring配置的有效性,并将标记配置中的类型违规等错误 . 如果没有这种工具支持,则无法在运行测试之前检查配置是否有效 .

这就是为什么'蛋糕'模式(正如Scala社区所知)是一个好主意的原因之一:组件之间的连线可以通过类型检查器进行检查 . 使用注释或XML没有这种好处 .

  • 它使程序的全局静态分析非常困难 .

像Spring或Guice这样的框架使得很难静态地确定容器创建的对象图形是什么样的 . 虽然它们在容器启动时创建对象图,但它们不提供描述/将要/将要创建的对象图的有用API .

3 years ago

当您经常使用技术来解决静态类型时,似乎静态类型语言的假设好处显着减少 . 我刚刚采访过的一家大型Java商店正在使用静态代码分析来映射它们的构建依赖关系......它必须解析所有Spring文件才能生效 .

3 years ago

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