【问题标题】:Is it OK for code that gets a dependency by dependency injection to use a default implementation of the dependency unless told otherwise?除非另有说明,否则通过依赖注入获得依赖的代码是否可以使用依赖的默认实现?
【发布时间】:2014-06-18 14:24:41
【问题描述】:

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

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(...);
}

这允许单元测试在不影响生产代码的情况下伪造它。

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

如果没有,有什么陷阱?

【问题讨论】:

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


    【解决方案1】:

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

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

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

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

    【讨论】:

      【解决方案2】:

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

      优点:

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

      缺点:

      • 将隐藏的合作者引入系统
      • 将 SUT 紧密耦合到具体类
        • 即使可以提供其他实现,但如果具体类发生更改,它将直接影响 SUT(例如,构造函数更改)
      • 也可以轻松地将 SUT 紧密耦合到其他类
        • 例如:您不仅创建依赖项,还创建所有依赖项

      一般来说,你应该只为非常轻量级的默认值保留它(想想Null Objects)。否则事情很容易迅速失控。

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

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

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

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

      【讨论】:

        猜你喜欢
        • 2011-10-27
        • 1970-01-01
        • 2012-11-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多