我试图在工作中引入依赖注入(DI)模式,我们的一位主要开发人员想知道:如果有的话,使用依赖注入模式的缺点是什么?
注意,如果可能的话,我在这里寻找一个详尽的列表,而不是关于这个主题的主观讨论。
澄清:我谈论的是依赖注入模式(参见Martin Fowler的这篇文章),而不是特定的框架,无论是基于xml的(如Spring)还是基于代码的(如Guice),还是“自滚动”的框架。
Reddit的subreddit /r/programming上正在进行一些深入的讨论/咆哮/辩论。
我试图在工作中引入依赖注入(DI)模式,我们的一位主要开发人员想知道:如果有的话,使用依赖注入模式的缺点是什么?
注意,如果可能的话,我在这里寻找一个详尽的列表,而不是关于这个主题的主观讨论。
澄清:我谈论的是依赖注入模式(参见Martin Fowler的这篇文章),而不是特定的框架,无论是基于xml的(如Spring)还是基于代码的(如Guice),还是“自滚动”的框架。
Reddit的subreddit /r/programming上正在进行一些深入的讨论/咆哮/辩论。
当前回答
有一件事让我对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>实例类作为依赖项。这将改变依赖对象的构造函数,并使其更加了解实现细节,例如对象构造开销,这也可以说是不可取的。
其他回答
代码可读性。由于依赖关系隐藏在XML文件中,因此您无法轻松地找出代码流。
我发现构造函数注入会导致构造函数又大又丑(我在整个代码库中都使用它——也许我的对象太细了?)此外,有时使用构造函数注入会导致可怕的循环依赖(尽管这种情况非常罕见),因此您可能会发现自己必须在更复杂的系统中拥有某种就绪状态生命周期,并进行几轮依赖注入。
然而,我更喜欢构造器注入而不是setter注入,因为一旦我的对象被构造,那么我就可以毫无疑问地知道它处于什么状态,它是在单元测试环境中还是加载到某个IOC容器中。这,以一种迂回的方式说,我觉得是setter注入的主要缺点。
(作为旁注,我确实发现整个主题相当“宗教”,但你的成就将随着开发团队的技术狂热程度而变化!)
我不认为存在这样的列表,但是试着阅读这些文章:
DI会模糊代码(如果你没有使用好的IDE) Bob叔叔认为,误用IoC会导致糟糕的代码。 需要注意过度设计和创造不必要的多功能性。
两件事:
它们需要额外的工具支持来检查配置是否有效。
例如,IntelliJ(商业版)支持检查Spring配置的有效性,并将标记出配置中的类型违反等错误。如果没有这种工具支持,就无法在运行测试之前检查配置是否有效。
这就是为什么“蛋糕”模式(Scala社区所熟知)是一个好主意的原因之一:组件之间的连接可以由类型检查器检查。而注释或XML则没有这种好处。
这使得程序的全局静态分析非常困难。
像Spring或Guice这样的框架很难静态地确定容器创建的对象图将是什么样子。尽管它们在容器启动时创建了一个对象图,但它们没有提供有用的api来描述将要创建的对象图。
没有任何DI的代码会面临众所周知的陷入意大利面条代码的风险——一些症状是类和方法太大,做太多,不容易更改、分解、重构或测试。
大量使用DI的代码可以是Ravioli代码,其中每个小类就像一个单独的Ravioli块——它只做一件小事,并且遵循单一责任原则,这是很好的。但是从类本身来看,很难看到系统作为一个整体在做什么,因为这取决于所有这些小部分是如何组合在一起的,这是很难看到的。它看起来就像一大堆小东西。
通过避免大型类中大量耦合代码的意大利面复杂性,您将面临另一种复杂性的风险,其中有许多简单的小类,它们之间的交互非常复杂。
我不认为这是一个致命的缺点- DI仍然是非常值得的。在某种程度上,小班级只做一件事的馄饨风格可能是好的。即使过度,我也不认为它像意大利面条代码那样糟糕。但是要避免它,第一步是要意识到它可能会走得太远。点击链接了解如何避免这种情况。