我试图在工作中引入依赖注入(DI)模式,我们的一位主要开发人员想知道:如果有的话,使用依赖注入模式的缺点是什么?

注意,如果可能的话,我在这里寻找一个详尽的列表,而不是关于这个主题的主观讨论。


澄清:我谈论的是依赖注入模式(参见Martin Fowler的这篇文章),而不是特定的框架,无论是基于xml的(如Spring)还是基于代码的(如Guice),还是“自滚动”的框架。


Reddit的subreddit /r/programming上正在进行一些深入的讨论/咆哮/辩论。


当前回答

同样的基本问题,你经常遇到的面向对象编程,样式规则和其他一切。这是可能的——事实上是非常常见的——做太多的抽象,添加太多的间接,并且通常在错误的地方过度地应用好的技术。

您应用的每个模式或其他构造都会带来复杂性。抽象和间接分散了信息,有时会移除了无关的细节,但有时也会让人更难理解到底发生了什么。你应用的每一条规则都会带来不灵活性,排除了可能是最佳方法的选择。

重点是编写能够完成这项工作的代码,并且是健壮的、可读的和可维护的。你是软件开发人员,而不是象牙塔建造者。

相关的链接

平台内效应

不要让建筑宇航员吓到你


可能依赖注入最简单的形式(别笑)是一个参数。依赖代码依赖于数据,而数据是通过传递参数的方式注入的。

是的,这很愚蠢,而且它没有解决依赖注入的面向对象问题,但是函数式程序员会告诉你(如果你有第一类函数)这是你唯一需要的依赖注入。这里的重点是举一个简单的例子,并展示潜在的问题。

以这个简单的传统函数为例。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的真实函数……

函数(winuser.h)

这有12个“依赖项”需要处理。例如,如果屏幕分辨率变得非常大,也许我们将需要64位的坐标值-和另一个版本的CreateWindowEx。是的,已经有一个旧版本仍然存在,它可能会在幕后映射到新版本……

创造温杜瓦宏(winuser宏)

这些“依赖关系”不仅仅是原始开发人员的问题——每个使用该接口的人都必须查找依赖关系是什么,它们是如何指定的,以及它们的含义,并确定为他们的应用程序做什么。这就是“明智的默认”可以让生活简单得多的地方。

面向对象的依赖注入在原理上没有什么不同。无论是在源代码文本中还是在开发人员时间中,编写类都是一种开销,如果编写该类是为了根据某些依赖对象规范提供依赖项,那么依赖对象将被锁定为支持该接口,即使需要替换该对象的实现。

这些都不应该被解读为依赖注入是不好的——远非如此。但是任何好的技术都可能被过度地应用在错误的地方。就像不是每个字符串都需要提取出来并转化为参数一样,也不是每个低级行为都需要从高级对象中提取出来并转化为可注入的依赖项。

其他回答

基于构造函数的依赖注入(没有神奇的“框架”的帮助)是构造OO代码的一种干净而有益的方式。在我所见过的最好的代码库中,经过多年与Martin Fowler的其他前同事的相处,我开始注意到大多数以这种方式编写的优秀类最终都只有一个doSomething方法。

那么,主要的缺点是,一旦您意识到这只是一种笨拙的、冗长的OO方式,将闭包编写为类,以获得函数式编程的好处,那么您编写OO代码的动机就会迅速消失。

如果您有一个自己开发的解决方案,依赖项就会在构造函数中直接出现。或者作为方法参数,这也不难发现。尽管框架管理的依赖关系,如果走到极端,就会开始变得像魔术一样。

然而,在太多的类中有太多的依赖项是一个明显的标志,说明你的类结构搞砸了。因此,在某种程度上,依赖注入(自行开发或框架管理)可以帮助发现那些可能隐藏在暗处的突出设计问题。


为了更好地说明第二点,这里是本文的一段摘录(原始来源),我完全相信这是构建任何系统的基本问题,而不仅仅是计算机系统。

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模式) 执行类型解析的DI容器或方法通常会导致轻微的运行时损失(非常可以忽略不计,但它确实存在)

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

这更像是吹毛求疵。但是依赖注入的一个缺点是,它使开发工具更难推理和导航代码。

具体来说,如果你在代码中控制-单击/命令-单击方法调用,它将带你到接口上的方法声明,而不是具体的实现。

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

而且,松散耦合代码的好处远远超过这一点,因此我称之为吹毛求疵。尽管我工作了很长时间,知道如果您试图引入依赖注入,可能会遇到这种情况。

事实上,我敢大胆地猜测,对于依赖注入的每一个“缺点”,您都会发现许多优点远远超过它。

两件事:

它们需要额外的工具支持来检查配置是否有效。

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

这就是为什么“蛋糕”模式(Scala社区所熟知)是一个好主意的原因之一:组件之间的连接可以由类型检查器检查。而注释或XML则没有这种好处。

这使得程序的全局静态分析非常困难。

像Spring或Guice这样的框架很难静态地确定容器创建的对象图将是什么样子。尽管它们在容器启动时创建了一个对象图,但它们没有提供有用的api来描述将要创建的对象图。