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

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


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


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


当前回答

以下几点:

DI增加了复杂性,通常是通过增加类的数量,因为责任分离得更多,这并不总是有益的 您的代码将(在某种程度上)耦合到您使用的依赖注入框架(或者更一般地说,如何决定实现DI模式) 执行类型解析的DI容器或方法通常会导致轻微的运行时损失(非常可以忽略不计,但它确实存在)

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

其他回答

依赖注入是一种技术或模式,与任何框架无关。您可以手动连接依赖项。DI帮助您实现SR(单一职责)和SoC(关注点分离)。DI会带来更好的设计。从我的观点和经验来看,没有坏处。就像任何其他模式一样,你可能会弄错或误用它(但在DI的情况下很难)。

如果您使用框架将DI作为原则引入到遗留应用程序中,那么您可能犯的最大错误就是将其误用为服务定位器。DI+框架本身是伟大的,只是让事情变得更好,我看到它!从组织的角度来看,每一个新的过程、技术、模式……都有共同的问题:

你必须训练你的团队 您必须更改您的应用程序(这包括风险)

一般来说,你必须投入时间和金钱,除此之外,没有任何负面影响,真的!

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

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

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

相关的链接

平台内效应

不要让建筑宇航员吓到你


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

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

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

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

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

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

这是我自己的第一反应:基本上任何模式都有同样的缺点。

学习是需要时间的 如果误解了,就会弊大于利 如果走到极端,工作量可能会超过收益

以下几点:

DI增加了复杂性,通常是通过增加类的数量,因为责任分离得更多,这并不总是有益的 您的代码将(在某种程度上)耦合到您使用的依赖注入框架(或者更一般地说,如何决定实现DI模式) 执行类型解析的DI容器或方法通常会导致轻微的运行时损失(非常可以忽略不计,但它确实存在)

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

我发现构造函数注入会导致构造函数又大又丑(我在整个代码库中都使用它——也许我的对象太细了?)此外,有时使用构造函数注入会导致可怕的循环依赖(尽管这种情况非常罕见),因此您可能会发现自己必须在更复杂的系统中拥有某种就绪状态生命周期,并进行几轮依赖注入。

然而,我更喜欢构造器注入而不是setter注入,因为一旦我的对象被构造,那么我就可以毫无疑问地知道它处于什么状态,它是在单元测试环境中还是加载到某个IOC容器中。这,以一种迂回的方式说,我觉得是setter注入的主要缺点。

(作为旁注,我确实发现整个主题相当“宗教”,但你的成就将随着开发团队的技术狂热程度而变化!)