假设我的生产代码是这样开始的:
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(...);
}
这允许单元测试伪造它,同时保持生产代码不受影响。
这是依赖注入的可接受实现吗?
如果没有,有什么陷阱?
答案 0 :(得分:2)
不,让调用者依赖于依赖项的具体实现并不是执行依赖项注入的最佳方法。组件实现应始终仅依赖于组件接口,而不是其他组件实现。
如果一个实现依赖于另一个,
我会通过将实现的生产选择放在代码或配置文件中或者指定所有实现的任何内容来解决您尝试解决的问题。
答案 1 :(得分:0)
这是一种有效的使用方法,但需要严格限制在某些情况下。有几个陷阱需要注意。
优点:
缺点:
一般情况下,您应该只保留非常轻量级的默认值(想想Null Objects)。否则,事情就很容易失控。
您尝试做的与使用默认构造函数类似,以使事情更方便一些。您应该确定方便是否会带来长期利益,或者它是否最终会再次困扰您。
即使您没有使用默认构造函数,我认为您应该考虑相似之处。 Mark Seemann建议默认构造函数could be a design smell :(强调我的)
默认构造函数是代码味道。你有它。这可能听起来很离谱,但考虑到这一点:面向对象是将行为和数据封装成有凝聚力的代码片段(类)。 封装意味着该类应该保护它封装的数据的完整性。当需要数据时,它通常必须通过构造函数提供。相反,默认构造函数意味着不需要外部数据。关于班级的不变量,这是一个相当弱的陈述。
考虑到所有这些,我认为您提供的示例将是一种冒险的方法,除非默认SMTP发件人是空对象(例如:实际上并不发送任何内容)。否则,composition root隐藏的信息太多,只会打开令人不快的意外的大门。