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

评论

相关(可能不是重复的)-stackoverflow.com/questions/2407540/…

听起来有点像金锤反模式。 zh.wikipedia.org/wiki/反模式

#1 楼

基本上,依赖项注入对对象的性质做出一些(通常但并非总是有效的)假设。如果错误,那么DI可能不是最佳解决方案:首先,最基本的说,DI认为对象实现的紧密耦合总是不好的。这就是依赖倒置原则的本质:“从不应该依赖于具体的东西,而不能依赖抽象”。

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

但是,设计永远都不可能对所有类型的变更都适用。如果IWriter接口本身的设计发生变化,要向Write()添加参数,则必须在实现对象/方法及其用法之上更改一个额外的代码对象(IWriter接口)。如果实际接口中的更改比对该接口的实现中更改的可能性更大,则松耦合(和DI-ing松耦合依赖项)可能导致比解决的问题更多。第二,也是必然的,DI认为从属类永远不是创建依赖的好地方。这遵循单一责任原则;如果您具有创建依赖项并也使用它的代码,则有两个原因可能导致依赖类发生更改(更改用法或实现),从而违反了SRP。

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


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

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

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


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



评论


SRP =单一责任原则,对于其他任何人都想知道。

–西奥多·默多克(Theodore Murdock)
2012-02-22 17:26

是的,LSP是Liskov替代原则;给定对A的依赖关系,该依赖关系应该能够由B满足,该继承自A的B无需进行任何其他更改。

– KeithS
2012年2月22日在17:29

在这种情况下(并且只有这种情况下),DI注入所产生的汇编膨胀比穷人的注入要少。我也从来没有遇到过由DI引起的瓶颈。考虑到维护成本,从不考虑cpu成本,因为代码总是可以优化的,但是无故这么做会带来成本

–CoffeDeveloper
2014年12月1日23:05

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

–Richard Tingle
15-10-25在10:14

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

–史密斯
17年5月25日在18:43

#2 楼

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

因此,依赖项注入(没有框架)同样有害,因为它很有帮助。客户端代码比对象本身更了解如何构造依赖关系,然后依赖关系注入确实可以减少耦合。例如,扫描程序并不了解如何获取或构造一个输入流来解析输入,或者不知道客户端代码要从哪个源解析输入,因此构造函数注入输入流是显而易见的解决方案。 />
测试是另一个理由,以便能够使用模拟依赖项。这应该意味着添加一个仅用于测试的额外构造函数,该测试仅允许注入依赖关系:如果您改为将构造函数更改为始终要求注入依赖关系,突然之间,您必须了解依赖关系的依赖关系才能构建直接依赖项,您将无法完成任何工作。

它会很有帮助,但是您绝对应该问每个依赖项,测试的好处值得吗?是否想在测试时模拟这种依赖关系?

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

与构造函数注入一样,我认为这样做的好处再次类似于这样做的成本:您不必知道谁在提供您依赖的数据,并且,如果有多个潜在的提供程序,则不必知道检查提供程序的首选顺序,请确保每个需要d的位置ata检查所有潜在的提供者,等等,因为所有这些都由依赖项注入平台进行了高层处理。我的印象是,当寻找所需的数据或服务的正确提供者的头痛比不知道何时出现故障而无法立即本地知道哪些代码提供了不良数据的麻烦时,它们提供的收益要高得多。导致您的代码后来出现故障。

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

评论


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

–埃里克·金(Eric King)
2012年2月21日在21:36

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

–西奥多·默多克(Theodore Murdock)
2012年2月22日在16:33

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

–西奥多·默多克(Theodore Murdock)
2012-2-22在16:37

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

–斯普拉
16-4-24在11:22



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

–西奥多·默多克(Theodore Murdock)
16年4月25日在20:14

#3 楼


如果要创建数据库实体,则应该有一些工厂类,而是将其注入到控制器中,
如果需要创建基本对象(如int或longs)。此外,您还应该“手动”创建大多数标准库对象,例如日期,向导等。
如果要注入配置字符串,最好注入一些配置对象(通常建议包装简单类型转换成有意义的对象:int temperatureInCelsiusDegrees-> CelciusDeegree temperature)

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

评论


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

– Jan Hudec
2013年9月23日15:02在

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

–科林
16年4月11日在21:52

相反,@ Colin绝对是Service Locator的反模式,而这里简而言之就是原因。

– Kyralessa
19年7月29日在14:50

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

–科林
19年7月29日在18:38



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

– Kyralessa
19年7月30日在7:30

#4 楼

当您无法通过使项目可维护和可测试而获得任何收益时。模式没有失败。这在多用户环境中尤其重要,因为您可以将逻辑与实现分离,您的代码可以由不同的团队成员和不同的项目反复使用。日志记录就是一个很好的例子,一个类的ILog接口比简单地插入您的日志记录框架du-jour的可维护性高一千倍,因为您不能保证另一个项目将使用相同的日志记录框架(如果一个。)。

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

总而言之,对任何方法论或模式都采取务实的方法是明智的,因为没有适用的方法100时间百分比。

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

评论


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

–Richard Tingle
15-10-25在10:27

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

–埃德·詹姆斯(Ed James)
15年10月28日在16:03

#5 楼

这不是一个完整的答案,只是另一点。 br />当您的应用程序启动多次且运行时间较短时(例如移动应用程序),您可能不希望使用容器。

评论


我看不到应用程序的实时与DI有何关系

– Michael Freidgeim
18年5月14日在22:21

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

–科雷·图吉(Koray Tugay)
18年5月14日在23:43

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

– Michael Freidgeim
18年5月16日在21:58

#6 楼

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

然后尝试使用DI重新编写它,并比较使用DI通常会看到的代码:


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

从我所看到的情况来看,几乎所有情况下,DI都会降低代码质量。

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

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

#7 楼

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

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



应该有理由进行介绍。

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


团队需要接受培训,并在船上。

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

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


您需要测试

虽然解耦通常是一件好事,但DI可以将依赖项的分辨率从编译时移到运行时。如果测试不好,这实际上是非常危险的。运行时解析失败可能是昂贵的跟踪和解决方法。 />

#8 楼

依赖项注入的替代方法是使用服务定位器。服务定位器更易于理解,调试,并且使对象的构造更简单,尤其是在您不使用DI框架的情况下。服务定位器是管理外部静态依赖项的好模式,例如,您必须将其传递到数据访问层中的每个对象中的数据库。重构到服务定位器,而不是依赖关系注入。您要做的就是用服务查找替换实例化,然后在单元测试中伪造服务。

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

评论


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

– Kyralessa
2012年2月20日在23:10

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

–埃里克·迪特里希(Erik Dietrich)
2012年2月21日,0:26

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

–加里特·霍尔(Garrett Hall)
2012年2月21日在14:13

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

– KeithS
2012年2月21日19:18

用马丁·福勒的话来说,“(DI和服务定位器)之间的选择比将配置与使用分开的原理重要”。 DI总是更好的想法就是浪费。如果在全球范围内使用服务,则使用DI会比较麻烦和脆弱。此外,DI不太适用于在运行时才知道实现的插件体系结构。想一想总是将其他参数传递给Debug.WriteLine()之类的东西是多么尴尬!

–科林
16-4-11在21:35