我试图在工作中引入依赖注入(DI)模式,我们的一位主要开发人员想知道:如果有的话,使用依赖注入模式的缺点是什么?
注意,如果可能的话,我在这里寻找一个详尽的列表,而不是关于这个主题的主观讨论。
澄清:我谈论的是依赖注入模式(参见Martin Fowler的这篇文章),而不是特定的框架,无论是基于xml的(如Spring)还是基于代码的(如Guice),还是“自滚动”的框架。
Reddit的subreddit /r/programming上正在进行一些深入的讨论/咆哮/辩论。
我试图在工作中引入依赖注入(DI)模式,我们的一位主要开发人员想知道:如果有的话,使用依赖注入模式的缺点是什么?
注意,如果可能的话,我在这里寻找一个详尽的列表,而不是关于这个主题的主观讨论。
澄清:我谈论的是依赖注入模式(参见Martin Fowler的这篇文章),而不是特定的框架,无论是基于xml的(如Spring)还是基于代码的(如Guice),还是“自滚动”的框架。
Reddit的subreddit /r/programming上正在进行一些深入的讨论/咆哮/辩论。
当前回答
这更像是吹毛求疵。但是依赖注入的一个缺点是,它使开发工具更难推理和导航代码。
具体来说,如果你在代码中控制-单击/命令-单击方法调用,它将带你到接口上的方法声明,而不是具体的实现。
这实际上是松散耦合代码(由接口设计的代码)的缺点,即使不使用依赖注入(即,即使只是使用工厂)也适用。但是依赖注入的出现真正鼓励了松耦合代码的普及,所以我想我应该提到它。
而且,松散耦合代码的好处远远超过这一点,因此我称之为吹毛求疵。尽管我工作了很长时间,知道如果您试图引入依赖注入,可能会遇到这种情况。
事实上,我敢大胆地猜测,对于依赖注入的每一个“缺点”,您都会发现许多优点远远超过它。
其他回答
它可以增加应用启动时间,因为IoC容器应该以适当的方式解析依赖关系,有时需要进行多次迭代。
这是我自己的第一反应:基本上任何模式都有同样的缺点。
学习是需要时间的 如果误解了,就会弊大于利 如果走到极端,工作量可能会超过收益
我不认为存在这样的列表,但是试着阅读这些文章:
DI会模糊代码(如果你没有使用好的IDE) Bob叔叔认为,误用IoC会导致糟糕的代码。 需要注意过度设计和创造不必要的多功能性。
同样的基本问题,你经常遇到的面向对象编程,样式规则和其他一切。这是可能的——事实上是非常常见的——做太多的抽象,添加太多的间接,并且通常在错误的地方过度地应用好的技术。
您应用的每个模式或其他构造都会带来复杂性。抽象和间接分散了信息,有时会移除了无关的细节,但有时也会让人更难理解到底发生了什么。你应用的每一条规则都会带来不灵活性,排除了可能是最佳方法的选择。
重点是编写能够完成这项工作的代码,并且是健壮的、可读的和可维护的。你是软件开发人员,而不是象牙塔建造者。
相关的链接
平台内效应
不要让建筑宇航员吓到你
可能依赖注入最简单的形式(别笑)是一个参数。依赖代码依赖于数据,而数据是通过传递参数的方式注入的。
是的,这很愚蠢,而且它没有解决依赖注入的面向对象问题,但是函数式程序员会告诉你(如果你有第一类函数)这是你唯一需要的依赖注入。这里的重点是举一个简单的例子,并展示潜在的问题。
以这个简单的传统函数为例。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有点不安,那就是假设所有注入的对象都很容易实例化,而且不会产生副作用- or -依赖关系被频繁使用,以至于它超过了任何相关的实例化成本。
当依赖项在消费类中不经常使用时,这一点可能很重要;比如IExceptionLogHandlerService。显然,这样的服务很少在类中调用(希望:))——大概只在需要记录异常时调用;然而,规范的构造器-注入模式……
Public Class MyClass
Private ReadOnly mExLogHandlerService As IExceptionLogHandlerService
Public Sub New(exLogHandlerService As IExceptionLogHandlerService)
Me.mExLogHandlerService = exLogHandlerService
End Sub
' ...
End Class
...要求提供该服务的“活动”实例,该死的成本/副作用。并不是说它可能会这样做,但是如果构建这个依赖实例涉及到服务/数据库命中,或配置文件查找,或在释放之前锁定资源,该怎么办呢?如果该服务是按需构建的、服务定位的或工厂生成的(它们都有各自的问题),那么您将只在必要时才承担构建成本。
Now, it is a generally accepted software design principle that constructing an object is cheap and doesn't produce side-effects. And while that's a nice notion, it isn't always the case. Using typical constructor-injection however basically demands that this is the case. Meaning when you create an implementation of a dependency, you have to design it with DI in mind. Maybe you would have made object-construction more costly to obtain benefits elsewhere, but if this implementation is going to be injected, it will likely force you to reconsider that design.
顺便说一下,某些技术可以通过允许延迟加载注入的依赖项来缓解这个问题,例如,提供一个Lazy<IService>实例类作为依赖项。这将改变依赖对象的构造函数,并使其更加了解实现细节,例如对象构造开销,这也可以说是不可取的。