穷人的依赖注入是将可测试性引入旧版应用程序的好方法吗?


14

在过去的一年中,我使用依赖注入和IOC容器创建了一个新系统。这教会了我很多关于DI的知识!

但是,即使在学习了概念和正确的模式之后,我仍然认为将代码解耦并将IOC容器引入旧版应用程序是一个挑战。该应用程序足够大,以至于真正的实现将是压倒性的。即使理解了价值并准予了时间。谁有时间抽出这样的东西?

当然,目标是将单元测试引入业务逻辑!
与防止测试的数据库调用交织在一起的业务逻辑。

我已经阅读了这些文章,并且了解了这篇Los Techies文章中描述的穷人依赖注入的危险。我了解它并没有真正分离任何东西。
我知道它可能涉及很多系统范围的重构,因为实现需要新的依赖项。我不会考虑在任何大小的新项目中使用它。

问题:可以使用穷人的DI 可测试性引入旧版应用程序并开始滚动吗?

另外,使用穷人的DI作为真正依赖注入的基层方法是否是教育该原理的需求和收益的有价值的方法?

您是否可以重构具有数据库调用依赖关系的方法,并将该调用抽象到接口后面?简单地拥有这种抽象将使该方法可测试,因为可以通过构造函数重载传递模拟实现。

将来,一旦获得支持者的支持,就可以对该项目进行更新以实现IOC容器,并且构造函数将在那里使用抽象。



2
请注意。I consider it a challenge to decouple code and introduce an IOC container into a legacy application当然是。它被称为技术债务。这就是为什么在进行任何重大改进之前,最好进行小型且连续的重构。减少主要的设计缺陷,转移到IoC的挑战将更少。
Laiv

Answers:


25

对NerdDinner中的“穷人注射”的批评与是否使用DI容器无关,而与正确设置类有关

他们在文章中指出

public class SearchController : Controller {

    IDinnerRepository dinnerRepository;

    public SearchController() : this(new DinnerRepository()) { }

    public SearchController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
}

这是不正确的,因为尽管第一个构造函数的确提供了用于构造类的便捷后备机制,但它也创建了对的紧密依赖DinnerRepository

正如Los Techies建议的那样,正确的补救措施当然不是添加DI容器,而是删除有问题的构造函数。

public class SearchController : Controller 
{
    IDinnerRepository dinnerRepository;

    public SearchController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
}

现在,其余类的依赖关系已正确反转。现在,您可以随意注入那些依赖项。


感谢您的想法!我了解“违规构造函数”以及应如何避免。我知道具有这种依赖性不会使任何问题脱钩,但是它确实允许进行单元测试。我正处于引入DI的早期阶段,并尽可能快地进行单元测试。我正在努力避免在游戏初期这样的DI / IOC容器或工厂的复杂化。如前所述,当对DI的支持增加时,我们可以实现一个Container并删除那些“有害的构造函数”。
Airn5475 '18

单元测试不需要默认构造函数。您可以将没有类的任何依赖项交给它,包括一个模拟对象。
罗伯特·哈维

2
@ Airn5475请注意,您也不要过度抽象。这些测试应该测试有意义的行为-测试XService将参数传递给an XRepository并返回其返回的值对任何人来说都不过分有用,也不会给您太多可扩展性。编写单元测试以测试有意义的行为,并确保您的组件不会过于狭窄,以至于您实际上将所有逻辑转移到了合成根。
蚂蚁P

我不明白包括有问题的构造函数将如何对单元测试有所帮助,肯定会相反吗?删除有问题的构造函数,以便正确实现DI,并在实例化对象时传递模拟的依赖关系。
罗布

@Rob:不是。这是默认构造函数站立有效对象的一种便捷方法。
罗伯特·哈维

16

您在这里对“穷人的DI”是一个错误的假设。

创建一个具有“快捷方式”构造函数且仍会创建耦合的类并不是穷人的DI。

不使用容器,而是手动创建所有注入和映射,这穷人的DI。

“穷人的DI”一词听起来很不好。因此,最近鼓励使用“ pure DI”一词,因为它听起来更积极,实际上更准确地描述了该过程。

使用穷人/纯DI来将DI引入现有应用程序中,这绝对是很好的方法,同时也是在许多新应用程序中使用DI的有效方法。正如您所说,在处理IoC容器的“魔术”责任之前,每个人都应该在至少一个项目上使用纯DI来真正理解DI的工作原理。


8
无论您偏爱“穷人”或“纯” DI,也可以缓解IoC容器的许多问题。我曾经将IoC容器用于所有内容,但是时间越长,我越希望完全避免使用它们。
蚂蚁P

7
@AntP,我与您同在:这些天我是100%纯DI,并积极避免使用容器。但据我所知,我(我们)在该分数上仅占少数。
David Arno

2
似乎非常可悲-当我离开IoC容器,模拟库之类的“魔术”阵营,发现自己正在拥抱OOP,封装,手动测试双打和状态验证时,似乎魔术的趋势仍在起来。还是+1。
蚂蚁P

3
@AntP&David:很高兴听到我在“ Pure DI”阵营中并不孤单。创建了DI容器以解决语言方面的缺陷。他们在这种意义上增加了价值。但是在我看来,真正的答案是修复语言(或迁移到其他语言),而不是在其基础上再做文章。
JimmyJames

1

遗留团队/代码库中的Paragidm转换极具风险:

每当您建议对遗留代码进行“改进”并且团队中有 “传统”程序员时,就是在告诉所有人“他们做错了”,并且在公司的余下时间成为“敌人

在任何情况下,使用DI框架作为锤子砸碎所有钉子只会使遗留代码变坏。这对个人而言也极具风险。

即使在像这样的最有限的情况下,它也可以在测试用例中使用,它们只会使那些测试用例成为“非标准”和“外来”代码,而这些代码充其量只会@Ignore在中断或恶化时被标记,甚至被不断抱怨。最有影响力的传统程序员会被“正确地”重写,而所有这些“浪费在单元测试上的时间”则完全归咎于您。

在一个业务应用程序中引入一个DI框架,甚至是“ Pure DI”的概念,更不用说大量的遗留代码库,而不会失去管理,团队,尤其是首席开发人员的赞助将是丧钟。在团队/公司上为您提供社交和政治支持。进行此类操作极具风险,并且可能以最坏的方式导致政治自杀。

依赖注入是一种寻找问题的解决方案。

如果您知道自己在做什么并且了解如何正确使用构造函数,则任何具有构造函数定义的语言都将按惯例使用依赖项注入,这就是很好的设计。

依赖注入仅在小剂量时适用于非常狭窄的范围:

  • 更改很多或具有静态绑定的替代实现的事物。

    • JDBC驱动程序就是一个很好的例子。
    • HTTP客户端可能因平台而异。
    • 记录系统因平台而异。
  • 具有可配置插件的插件系统,可以在框架的配置代码中定义这些插件系统,并在启动时自动发现它们,并在程序运行时动态加载/重新加载它们。


10
DI的杀手级应用是单元测试。正如您所指出的,大多数软件本身并不需要很多DI。但是测试也是该代码的客户,因此我们应该设计可测试性。特别是,这意味着在我们的设计中放置接缝,以便测试可以观察到这种行为。这不需要花哨的DI框架,但确实需要使相关的依赖关系明确且可替换。
阿蒙(Amon)

4
如果我们的开发人员知道前进是否可以改变,那将是成功的一半。我从事的开发工作很长,许多所谓的“固定”东西都在变化。使用DI进行将来的证明并不是一件坏事。始终在无处不在使用它,这是对货物崇拜的编程。
罗比·迪

测试过于复杂只会意味着它们过时并被标记为忽​​略,而不是将来进行更新。遗留代码中的DI是错误的经济做法。他们将要做的只是使您成为“坏人”,因为他们会将所有这些“没人能理解的非标准,过于复杂的代码”放入代码库。你被警告了。

4
@JarrodRoberson,那确实是一个有毒的环境。优秀开发人员的主要标志之一是,他们回头看过去编写的代码,然后认为:“哦,我真的写过吗?那太错了!” 那是因为他们已经成长为开发人员并学习了新知识。有人看了他们五年前编写的代码,却发现代码没错,这五年来却没有学到任何东西。因此,如果告诉人们他们过去做错了,会使您成为敌人,那么请迅速逃离该公司,因为他们是一支死胡同的团队,将阻止您并将您降低到可怜的水平。
David Arno

1
不知道我是否同意坏人的话,但对我而言,主要的收获是您不需要DI即可进行单元测试。实施DI以使代码更具可测试性只是解决问题。
Ant P
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.