什么时候不适合使用依赖项注入模式?


124

自从学习(并喜欢)自动化测试以来,我发现自己几乎在每个项目中都使用了依赖注入模式。在进行自动化测试时,使用这种模式是否总是合适的?您是否应该避免使用依赖注入?


1
相关(可能不重复) - stackoverflow.com/questions/2407540/...
Steve314

3
听起来有点像金锤反模式。 en.wikipedia.org/wiki/Anti-pattern
Cuga 2013年

Answers:


123

基本上,依赖注入会对对象的性质做出一些(通常但并非总是有效的)假设。如果错误,那么DI可能不是最佳解决方案:

  • 首先,最重要的是,DI假设对象实现的紧密结合总是不好的。这就是依赖倒置原则的本质:“从不应该依赖于具体的东西,而不能依赖抽象”。

    这将根据具体实现的更改关闭要更改的从属对象;如果需要将输出转至文件,则具体取决于ConsoleWriter的类将需要更改,但是,如果该类仅依赖于暴露Write()方法的IWriter,则可以用FileWriter替换当前使用的ConsoleWriter,并且依赖类不会知道区别(Liskhov替代原理)。

    但是,设计永远都不可能对所有类型的变更都是封闭的。如果IWriter接口本身的设计发生更改,则要向Write()添加参数,则必须在实现对象/方法及其用法的基础上更改一个额外的代码对象(IWriter接口)。如果实际接口中的更改比对所述接口的实现进行更改的可能性更大,那么松散耦合(和DI-ing松散耦合的依赖项)可能会导致更多问题,而不是解决的问题。

  • 其次,推论是,DI假定从属类永远不是创建依赖的好地方。这遵循单一责任原则;如果您具有创建依赖项并也使用它的代码,则依赖项类可能不得不更改(更改用法或实现)有两个原因,这违反了SRP。

    但是,再次,为DI添加间接层可以解决不存在的问题。如果将逻辑封装在依赖关系中是逻辑上的,但是该逻辑是依赖关系的唯一这样的实现,那么编写依赖关系的松耦合解析(注入,服务位置,工厂)将比编写代码更痛苦。只是使用new而忘记它。

  • 最后,DI的本质是集中所有依赖及其实现的知识。这会增加执行注入的程序集必须具有的引用数,并且在大多数情况下不会减少实际依赖类的程序集所需的引用数。

    SOMETHING,SOMEWHERE,必须具有相关性,相关性接口和相关性实现的知识,才能“连接点”并满足该相关性。DI倾向于将所有这些知识放在一个非常高的层次上,或者放在IoC容器中,或者放在创建“主要”对象的代码中,例如必须混合(或提供工厂方法)依赖项的“主要”对象或主窗体。这可以在应用程序的高层放置大量必须紧密耦合的代码和大量程序​​集引用,而这仅需要这些知识即可将其从实际的依赖类中“隐藏”(从非常基本的角度来看,了解这些知识的最佳地点;使用地点)。

    通常,它也不会从代码的最下面移除引用。一个依赖项仍然必须引用包含该接口的接口的依赖项,该库位于以下三个位置之一:

    • 全部集中在一个以应用程序为中心的单一“接口”程序集中,
    • 每一个都与主要实现并排,消除了不必在依赖项更改时重新编译依赖项的优点,或者
    • 在具有高凝聚力的组件中一个或两个,会大大增加组件数量,大大增加“完全构建”时间并降低应用程序性能。

    所有这些,再次解决了可能没有的地方的问题。


1
SRP =单一责任原则,对于其他任何人都想知道。
西奥多·默多克

1
是的,LSP是Liskov替代原则。给定对A的依赖关系,则该依赖关系应该能够由B满足,该依赖关系派生自A,而无需进行任何其他更改。
KeithS 2012年

在这种情况下(并且只有这种情况下),依赖注入特别地帮助您从B类从D类获取A类,在这种情况下(且仅在这种情况下),DI框架生成的组装膨胀比穷人的注入要少。我也从来没有遇到过由DI引起的瓶颈。.考虑到维护成本,从不考虑cpu成本,因为代码总是可以优化的,但是无缘无故地这样做是有成本的
GameDeveloper 2014年

另一个假设是“如果依赖关系发生变化,那么它到处都会发生变化”?否则,无论如何,您都必须查看所有消费类
Richard Tingle

3
此答案无法解决注入依赖项与对其进行硬编码的可测试性影响。另见显式依赖原则(deviq.com/explicit-dependencies-principle
ssmith

30

在依赖关系注入框架之外,依赖关系注入(通过构造函数注入或setter注入)几乎是一个零和游戏:您减少了对象A与它的依赖关系B之间的耦合,但是现在任何需要A实例的对象都必须这样做还要构造对象B。

您已经稍微减少了A和B之间的耦合,但是减少了A的封装,并通过将A和必须构造A实例的任何类也耦合到A的依赖关系来增加了它们之间的耦合。

因此,依赖项注入(没有框架)同样有害,因为它很有帮助。

但是,额外的费用通常很容易被证明是合理的:如果客户端代码比对象本身更了解如何构造依赖项,那么依赖项注入的确会减少耦合。例如,扫描程序对如何获取或构造一个输入流来解析输入内容,或者客户代码想要从哪个源解析输入内容的知识并不多,因此构造函数注入输入流是显而易见的解决方案。

为了能够使用模拟依赖项,测试是另一个理由。那应该意味着添加一个仅用于测试的额外构造函数,该测试仅允许注入依赖关系:如果您改为将构造函数更改为始终要求注入依赖关系,突然之间,您必须了解依赖关系的依赖关系才能构建您的依赖关系直接依赖关系,您将无法完成任何工作。

它可能会有所帮助,但是您一定要问问自己每个依赖项,测试的好处值得吗?我真的要在测试时模拟这个依赖项吗?

当添加依赖项注入框架,并且依赖项的构造不是委托给客户端代码而是委托给框架时,成本/收益分析将发生很大变化。

在依赖注入框架中,权衡有所不同。通过注入依赖项而失去的是能够轻松了解您所依赖的实现,并将决定所依赖的依赖项的职责转移到一些自动解决程序上的能力(例如,如果我们需要@ Inject'ed Foo ,必须有@Provides Foo的东西,并且注入的依赖项可用),或者必须有一些高级配置文件,该文件规定应为每种资源使用哪种提供程序,或者两者的某种混合(例如,可能存在是针对依赖项的自动解析过程,可以在需要时使用配置文件将其覆盖)。

就像在构造函数注入中一样,我认为这样做的好处再次与这样做的成本非常相似:您不必知道谁在提供您所依赖的数据,以及是否存在多重潜力提供者,您不必知道检查提供者的首选顺序,确保每个需要数据的位置都需要检查所有潜在的提供者,依此类推,因为所有这些都由依赖项注入在高层处理平台。

虽然我个人没有大量的DI框架经验,但我的印象是,当寻找所需的正确数据或服务提供者的头痛比头痛时要付出更高的代价时,它们提供的好处多于成本。当某件事失败时,不能立即本地知道哪些代码提供了导致以后代码失败的错误数据。

在某些情况下,当DI框架出现时,已经采用了其他掩盖依赖性的模式(例如,服务定位符)(也许还证明了它们的价值),之所以采用DI框架是因为它们提供了一些竞争优势,例如要求更少的样板代码,或者在有必要确定实际使用的提供者时可能做得更少,从而使依赖提供者变得晦涩难懂。


6
只是对测试中的依赖关系模拟进行快速评论,然后“您必须了解依赖关系的依赖关系” ...根本不对。如果注入了模拟,则无需了解或关心某些具体实现的依赖性。只需知道被测类的直接依赖关系即可,这些依赖关系由模拟程序满足。
埃里克·金

6
您误会了,我不是在谈论注入模拟程序,而是在谈论真正的代码。考虑具有依赖项B的类A,而依赖项B又具有依赖项C,而依赖项C又具有依赖项D。没有DI,A构造B,B构造C,C构造D。通过构造注入,要构造A,必须首先构造B,要构造B,您必须首先构造C,并且首先要构造C,因此,类A现在必须了解D,即依赖关系的依赖关系,才能构建B。这导致过多的耦合。
西奥多·默多克

1
如果有一个额外的构造函数用于仅允许插入依赖项的测试,则不会有太多的成本。我会尝试修改我所说的。
西奥多·默多克

3
依赖注入剂量仅在零和博弈中移动依赖。您将依赖项移动到它们已经存在的地方:主项目。您甚至可以使用基于约定的方法来确保仅在运行时解决依赖项,例如,通过指定名称空间等具体的类来确定。我不得不说这是一个幼稚的答案
Sprague

2
如果在不应该应用的情况下应用@Sprague依赖注入,则它会在稍微为负数的博弈中绕过依赖关系。这个答案的目的是提醒人们依赖注入并不是天生的,它是一种在正确的情况下非常有用的设计模式,但是在正常情况下(至少在我从事过的编程领域中)这是不合理的成本,据我所知,在某些领域它比在其他领域更有用)。DI有时成为流行语,而其本身就是目的,这没有切合实际。
西奥多·默多克

11
  1. 如果要创建数据库实体,则应该有一些工厂类,将其注入到控制器中,

  2. 如果您需要创建int或longs之类的原始对象。另外,您应该“手动”创建大多数标准库对象,例如日期,向导等。

  3. 如果要注入配置字符串,最好注入一些配置对象(通常,建议将简单类型包装到有意义的对象中:int temperatureInCelsiusDegrees-> CelciusDeegree temperature)

并且不要将Service locator用作依赖项注入的替代方法,它是一种反模式,更多信息:http : //blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx


所有要点是关于使用不同形式的注射,而不是完全不使用它。+1虽然链接。
Jan Hudec 2013年

3
服务定位器不是反模式,您链接到的博客的人也不是模式专家。他使StackExchange闻名于世,这对软件设计不利。Martin Fowler(企业应用程序体系结构主要模式的作者)对此有更合理的看法:martinfowler.com/articles/injection.html
Colin,

1
相反,@ Colin绝对是Service Locator的反模式,这里简而言之就是为什么
Kyralessa

@Kyralessa-您意识到DI框架在内部使用Service Locator模式来查找服务,对吗?在某些情况下,这两者都是合适的,尽管某些人想抛弃StackExchange的仇恨,但两者都不是反模式。
科林

当然,我的汽车在内部使用活塞和火花塞以及许多其他东西,但是对于那些只想开车的普通人来说,这不是一个很好的界面。
Kyralessa

8

当您无法通过使项目可维护和可测试而获得任何收益时。

认真地说,我总体上喜欢IoC和DI,我想说98%的时间我都会使用该模式而不会失败。这在多用户环境中尤其重要,因为您的代码将逻辑与实现分开,因此您的代码可以由不同的团队成员和不同的项目反复使用。日志记录就是一个很好的例子,一个类的ILog接口比简单地插入您的日志记录框架du-jour的可维护性高一千倍,因为您不能保证另一个项目将使用相同的日志记录框架(如果使用一个!

但是,有时它不是适用的模式。例如,由不可重写的初始化程序在静态上下文中实现的功能入口点(WebMethods,我在看您,但是Program类中的Main()方法是另一个示例),根本无法在初始化时注入依赖项时间。我还要说原型或任何扔掉的调查性代码也是不好的选择。DI的好处几乎是中长期的好处(可测试性和可维护性),如果您确定会在一周之内扔掉大部分代码,那么我想说通过隔离您不会获得任何好处依赖关系,只需花费您通常用于测试和隔离依赖关系的时间即可使代码正常工作。

总而言之,对任何方法论或模式都采取务实的态度是明智的,因为100%的情况下都不适用。

需要注意的一件事是您对自动化测试的评论:我对此的定义是自动化功能测试,例如,如果您处于Web上下文中,则为脚本化硒测试。这些通常是完全黑盒测试,无需了解代码的内部工作原理。如果您指的是单元测试或集成测试,那么我会说,DI模式几乎总是适用于任何严重依赖这种白盒测试的项目,因为例如,它允许您测试诸如不需要数据库就可以接触数据库的方法。


我不明白您对日志的意思。当然,所有记录器都不会遵循相同的接口,因此您必须编写自己的包装程序才能在此项目记录方式和特定记录器之间进行转换(假设您希望能够轻松更改记录器)。那么,那之后DI给您什么?
理查德·廷格

@RichardTingle我想说的是,您应该将日志记录包装器定义为一个接口,然后为每个外部日志记录服务编写该接口的轻量级实现,而不是使用包含多个抽象的单个日志记录类。您提出的是相同的概念,但在不同的层次上进行了抽象。
Ed James

1

其他答案集中在技术方面时,我想添加一个实际的方面。

多年来,我得出的结论是,要成功实施依赖注入,必须满足一些实际要求。

  1. 应该有理由进行介绍。

    这听起来很明显,但是如果您的代码仅从数据库中获取内容并没有任何逻辑地将其返回,则添加DI容器会使事情变得更复杂而没有实际好处。集成测试在这里更重要。

  2. 该团队需要接受培训并入职。

    除非团队的大多数成员都参与其中并且了解DI,否则添加控制容器的反转将成为另一种处理事情的方法,并使代码库更加复杂。

    如果DI是由团队的新成员介绍的,因为他们了解并喜欢它,并且只想表明自己是优秀的,并且团队没有积极参与,则存在真正降低其质量的风险。编码。

  3. 你需要测试

    虽然去耦通常是一件好事,但DI可以将依赖项的分辨率从编译时移到运行时。如果测试不好,这实际上是非常危险的。运行时解析失败可能是昂贵的跟踪和解决方案。
    (从您的测试中可以很明显地看出来,但是许多团队并未按照DI的要求进行测试。)


1

这不是一个完整的答案,只是另一点。

如果您的应用程序只能启动一次,并且可以运行很长时间(例如Web应用程序),那么DI可能很好。

当您的应用程序启动多次且运行时间较短时(例如移动应用程序),您可能不希望使用该容器。


我没有看到相关的DI应用的如何生存时间
迈克尔Freidgeim

@MichaelFreidgeim初始化上下文需要花费时间,通常DI容器非常重,例如Spring。制作一个只有一堂课的hello world应用程序,并用Spring制作一堂课,并同时开始10次,您将明白我的意思。
Koray Tugay

听起来像是单个DI容器有问题。在.Net世界中,我还没有听说过初始化时间是DI容器的基本问题
Michael Freidgeim,

1

尝试使用基本的OOP原则:使用继承提取通用功能,封装(隐藏)应使用私有/内部/受保护的成员/类型保护的东西,以防止外界的侵害。使用任何功能强大的测试框架仅注入测试代码,例如https://www.typemock.com/https://www.telerik.com/products/mocking.aspx

然后尝试用DI重新编写它,并比较代码,这些通常会在DI中看到:

  1. 您有更多接口(更多类型)
  2. 您已经创建了公共方法签名的重复副本,并且必须对其进行双重维护(不能简单地更改某个参数一次,而必须进行两次更改,基本上所有重构和导航的可能性都变得更加复杂)
  3. 您已将一些编译错误移至运行时失败(使用DI时,您只能在编码过程中忽略某些依赖关系,并且不确定在测试过程中是否会暴露该依赖关系)
  4. 您已打开封装。现在受保护的成员,内部类型等已公开
  5. 总代码量增加

从我所看到的情况来看,我几乎总是说,DI导致代码质量下降。

但是,如果您在类声明中仅使用“公共”访问修饰符,和/或为成员使用了公共/私有修饰符,并且/或者您没有选择购买昂贵的测试工具,而同时又需要进行单元测试,那么不能用集成测试代替,和/或您已经拥有要注入的类的接口,DI是一个不错的选择!

ps可能我会在这篇文章中有很多缺点,我相信是因为大多数现代开发人员只是不了解如何以及为什么使用内部关键字,以及如何减少组件的耦合,最后为什么要减少它的耦合),最后,只是尝试编码和比较


0

依赖注入的一种替代方法是使用服务定位器。服务定位器更易于理解,调试,并且使对象的构造更简单,尤其是在您不使用DI框架的情况下。服务定位器是管理外部静态依赖项的好模式,例如,否则您必须将其传递到数据访问层中的每个对象中的数据库。

重构遗留代码,它往往比依赖注入更容易重构为一个服务定位器。您要做的就是用服务查找替换实例化,然后在单元测试中伪造服务。

但是,服务定位器有一些缺点。知道类的关系更加困难,因为依赖关系隐藏在类的实现中,而不是构造函数或setter中。创建依赖于同一服务的不同实现的两个对象是困难或不可能的。

TLDR:如果您的类具有静态依赖关系,或者您要重构遗留代码,那么服务定位器可以说比DI更好。


12
服务定位器隐藏代码中的依赖项。这不是使用的好模式。
Kyralessa

当涉及到静态依赖关系时,我宁愿看到在实现接口的基础上使用外观。然后可以将它们注入依赖项,它们会落在您的静态手榴弹上。
Erik Dietrich '02

@Kyralessa我同意,服务定位器有很多缺点,并且DI总是总是可取的。但是,我相信,与所有编程原则一样,该规则也有一些例外。
加勒特音乐厅

服务位置的主要接受用法是在“策略”模式内,在该模式中,将为“策略选择器”类提供足够的逻辑以找到要使用的策略,然后将其交还或将呼叫传递给该策略。即使在这种情况下,策略选择器也可以是IoC容器提供的工厂方法的外观,该容器具有相同的逻辑。您将其分解的原因是将逻辑放在它所属的位置,而不是最隐藏的位置。
KeithS 2012年

用Martin Fowler的话来说,“(DI和Service Locator)之间的选择不如将配置与使用分开的原理重要”。DI总是更好的想法就是浪费。如果在全球范围内使用服务,则使用DI会比较麻烦和脆弱。此外,DI对于在运行时才知道实现的插件体系结构不太有利。想一想总是将其他参数传递给Debug.WriteLine()之类的东西是多么尴尬!
科林
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.