通过依赖注入获取依赖关系的代码是否可以使用依赖项的默认实现,除非另有说明?

时间:2014-05-02 00:43:29

标签: unit-testing interface dependency-injection optional-parameters

假设我的生产代码是这样开始的:

public class SmtpSender
{
    ....
}

public void DoCoolThing()
{
    var smtpSender = new SmtpSender("mail.blah.com"....);
    smtpSender.Send(...)
}

我有一个脑波,并决定使用假的SmtpSender和依赖注入单元测试这个功能:

public interface ISmtpSender
{
...
}

public class SmtpSender : ISmtpSender
{
...
}

public class FakeSmtpSender : ISmtpSender
{
...
}

public void DoCoolThing(ISmtpSender smtpSender)
{
    smtpSender.Send(...)
}

然后我想等一下,我的所有生产代码总是想要真正的代码。那么为什么不将参数设为可选参数,如果没有提供,则用默认的SmtpSender填充它?

public void DoCallThing(ISmtpSender smtpSender = null)
{
    smtpSender = smtpSender ?? new SmtpSender("...");
    smtpSender.Send(...);
}

这允许单元测试伪造它,同时保持生产代码不受影响。

这是依赖注入的可接受实现吗?

如果没有,有什么陷阱?

2 个答案:

答案 0 :(得分:2)

不,让调用者依赖于依赖项的具体实现并不是执行依赖项注入的最佳方法。组件实现应始终仅依赖于组件接口,而不是其他组件实现。

如果一个实现依赖于另一个,

  • 它为调用者提供了一种完全不同的责任,而不是配置自己的责任。因此呼叫者将不那么连贯。
  • 它为调用者提供了一个它本来不具备的类的依赖。这意味着如果依赖项被破坏,调用者的测试可能会中断,依赖项所需的类在加载调用者时而不是在需要时加载,并且在编译语言中会存在编译时依赖性。所有这些都使得开发大型系统变得更加困难。
  • 如果多个调用者想要相同的依赖关系,则每个这样的调用者可能会使用相同的技巧。然后,您将在调用者之间复制具体的依赖关系,而不是在一个地方具有具体的依赖关系(无论您何时指定所有注入的依赖关系)。
  • 它可以防止您在实现中使用有用的循环依赖。偶尔我需要实现以循环方式相互依赖。纯依赖注入使得相对安全:实现总是依赖于接口,因此没有编译时循环。 (依赖注入框架使得构建所有实现成为可能,尽管通过插入代理来实现循环。)具有特定依赖性,您将永远无法做到这一点。

我会通过将实现的生产选择放在代码或配置文件中或者指定所有实现的任何内容来解决您尝试解决的问题。

答案 1 :(得分:0)

这是一种有效的使用方法,但需要严格限制在某些情况下。有几个陷阱需要注意。

优点:

    代码中的
  • Establishes a seam,允许将不同的实现用作协作者
  • 允许客户使用您的代码,而无需指定协作者 - 特别是如果大多数客户最终使用完全相同的协作者

缺点:

  • 向系统中引入隐藏的协作者
  • 将SUT与混凝土课程紧密结合
    • 即使可以提供其他实现,如果具体类更改,它将直接影响SUT(例如,构造函数更改)
  • 也可以轻松地将SUT紧密地耦合到其他类
    • 例如:您不仅创建了依赖关系,还创建了所有 依赖关系

一般情况下,您应该只保留非常轻量级的默认值(想想Null Objects)。否则,事情就很容易失控。

您尝试做的与使用默认构造函数类似,以使事情更方便一些。您应该确定方便是否会带来长期利益,或者它是否最终会再次困扰您。

即使您没有使用默认构造函数,我认为您应该考虑相似之处。 Mark Seemann建议默认构造函数could be a design smell :(强调我的)

  

默认构造函数是代码味道。你有它。这可能听起来很离谱,但考虑到这一点:面向对象是将行为和数据封装成有凝聚力的代码片段(类)。 封装意味着该类应该保护它封装的数据的完整性。当需要数据时,它通常必须通过构造函数提供。相反,默认构造函数意味着不需要外部数据。关于班级的不变量,这是一个相当弱的陈述。

考虑到所有这些,我认为您提供的示例将是一种冒险的方法,除非默认SMTP发件人是空对象(例如:实际上并不发送任何内容)。否则,composition root隐藏的信息太多,只会打开令人不快的意外的大门。