我正在尝试在工作中引入依赖注入(DI)模式,我们的一位主要开发人员想知道:使用依赖注入模式有哪些缺点?
请注意,我在这里寻求的是尽可能详尽的列表,而不是主观讨论。
澄清:我谈论的是依赖注入模式(参见Martin Fowler的这篇文章),而不是特定的框架,无论是基于XML的(例如Spring)还是基于代码的(例如Guice),或者"自制"的。
一些很棒的进一步讨论/发泄/辩论正在Reddit的子版块/r/programming进行。
我正在尝试在工作中引入依赖注入(DI)模式,我们的一位主要开发人员想知道:使用依赖注入模式有哪些缺点?
请注意,我在这里寻求的是尽可能详尽的列表,而不是主观讨论。
一些很棒的进一步讨论/发泄/辩论正在Reddit的子版块/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;
}
面向对象的依赖注入原则上并没有什么不同。编写一个类既是源代码文本的开销,也是开发人员时间的开销。如果该类是根据某些受支持对象规格提供依赖项的,则即使需要替换该对象的实现,依赖对象也被锁定为支持该接口。
这并不意味着声称依赖注入是不好的-相反,它非常有用。但是任何好的技术都可以过度应用和使用不当。就像不需要将每个字符串都提取出来并转换为参数一样,不需要将每个低级行为从高级对象中提取出来并转换为可注入的依赖项。
这是我的初始反应:基本上与任何模式相同的缺点。
控制反转(Inversion of Control)最大的“缺点”(虽然这不完全等同于依赖注入,但相似度很高)就是它往往会消除算法的总览性。然而,当你有松散耦合的代码时,这基本上就是发生的事情——只能在一个地方查看算法是一种紧密耦合的产物。
我过去6个月一直大量使用Guice (Java DI框架)。总体而言,我认为它非常好(特别是从测试的角度),但也有一些缺点。其中最明显的是:
现在我抱怨完了。让我说一下,我将继续(自愿地)在我的当前项目和可能的下一个项目中使用Guice。依赖注入是一种伟大且非常强大的模式。但它确实可能会使人感到困惑,而您几乎肯定会花费一些时间在所选择的任何依赖注入框架上发泄不满。
此外,我同意其他帖子的观点,即依赖注入可能被过度使用。
我认为没有这样的列表,但你可以尝试阅读以下文章:
没有任何DI的代码可能会陷入意大利面条式代码,一些症状是类和方法过大,做了太多事情,难以轻松更改、分解、重构或测试。
使用了很多DI的代码可以成为意大利瑞士卷式代码,其中每个小类都像一个独立的意大利瑞士卷馅料 - 它只做一件小事,并且遵循单一职责原则,这是好的。但仅看单个类很难看出整个系统的功能,因为这取决于所有这些许多小部件如何配合,这很难看出来。它看起来只像是一堆小东西。
通过避免大类中耦合代码的意大利面条般的复杂性,您将面临另一种复杂性,即有很多简单的小类,它们之间的交互是复杂的。
我不认为这是致命的缺点-DI仍然非常值得。一定程度上具有只做一件事的小类的意大利瑞士卷式风格可能是有好处的。即使过度使用,我认为它也不像意大利面条代码那样糟糕。但是意识到它会被过分使用是避免它的第一步。请参阅讨论如何避免它的链接。
仅通过实现依赖注入而没有真正解耦你的代码,这种假象是最危险的。我认为这就是DI最危险的地方。
如果您有自己开发的解决方案,依赖项就在构造函数中或者作为方法参数出现,这不太难发现。但是,过多的由框架管理的依赖项可能会开始显得像魔法。
然而,一个类中有太多依赖关系,这是类结构混乱的明确标志。因此,无论是自己开发的还是框架管理的依赖注入都可以帮助揭示那些可能隐藏在黑暗中的明显设计问题。
为了更好地说明第二点,这里引用了本文 (原始来源) 中的一段摘录,我完全相信这是建立任何系统的根本问题,不仅仅是计算机系统。
假设你想设计一个大学校园。你必须将设计的一部分委托给学生和教授,否则物理建筑就不适合物理人员使用。没有一位建筑师知道物理人员需要什么样的条件,以便自己完成所有工作。但是您不能将每个房间的设计都委托给其占用者,否则您将得到一堆废墟。
如何在大型层次结构中通过分配设计责任来维护整体设计的一致性和和谐性?这是Alexander试图解决的建筑设计问题,但也是计算机系统开发的根本问题。
依赖注入能够解决这个问题吗?不是。但它确实可以帮助您清楚地看到您是否正在尝试将每个房间的设计责任委托给其占用者。
在依赖注入中,让我有点不放心的是假设所有被注入的对象都是廉价实例且没有副作用-OR-这个依赖项在使用时频率很高,以至于它超过了任何相关的实例化成本。
当一个依赖项在消费类中不是频繁地使用时,比如像IExceptionLogHandlerService
这样的东西。显然,这样的服务在类中很少被调用(希望如此:)),可能只在需要记录日志的异常上调用;然而典型的构造函数注入模式......
Public Class MyClass
Private ReadOnly mExLogHandlerService As IExceptionLogHandlerService
Public Sub New(exLogHandlerService As IExceptionLogHandlerService)
Me.mExLogHandlerService = exLogHandlerService
End Sub
' ...
End Class
要求提供“活”服务的实例,即使获取该实例需要付出成本/副作用。如果构造该依赖项实例涉及服务/数据库调用、配置文件查找或锁定资源直到被处理掉,那又怎样呢?如果将该服务改为按需构建、定位服务或工厂生成(它们都有自己的问题),则只有在必要时才会付出构建成本。
现在,通常接受的软件设计原则是,构造对象是廉价的且不会产生副作用。虽然这是一个很好的想法,但并不总是如此。然而,使用典型的构造函数注入基本上要求这种情况发生。这意味着当您创建依赖项的实现时,必须考虑到DI。也许您会使对象构造更加昂贵以获得其他优势,但如果该实现将被注入,则可能会迫使您重新考虑该设计。
顺便说一句,某些技术可以通过允许延迟加载注入的依赖项来缓解这个问题,例如,将Lazy<IService>
实例作为依赖项提供给类。这将改变你的依赖对象的构造函数,并使它们更加了解实现细节,例如对象构造费用,这可能并不理想。
this.errorLogger.WriteError(ex)
实例化实现IErrorLogger的类。 - John Bonfardeci