构造函数注入和默认重载

6

假设我们有一个

public interface ITimestampProvider
{
    DateTime GetTimestamp();
}

以及一个使用该类的类

public class Timestamped
{
    private ITimestampProvider _timestampProvider

    public Timestamped(ITimestampProvider timestampProvider)
    {
        // arg null check

        _timestampProvider = timestampProvider;
    }

    public DateTime Timestamp { get; private set; }

    public void Stamp()
    {
        this.Timestamp = _timestampProvider.GetTimestamp();
    }
}

和默认实现:

public sealed class SystemTimestampProvider : ITimestampProvider
{
    public DateTime GetTimestamp()
    {
        return DateTime.Now;
    }
}

这个构造函数的引入是有益还是有害的?
public Timestamped() : this(new SystemTimestampProvider())
{}

这是一个一般性问题,即时间戳并不是有趣的部分。


你在使用哪种依赖注入的方式,CAB?Castle? - Robert Wagner
针对这个问题,没有特定目的。这是一个一般性的API查询。我已经更新了问题,去掉了“注入”的含义。 - Bryan Watts
6个回答

7

我认为这取决于具体情况,基本上是由使用代码的消费者(库 vs 应用程序)以及是否使用IoC容器来确定的。

  • 如果您正在使用IoC容器,并且这不是公共API的一部分,则让容器来处理重活,只需使用单个构造函数。添加无参数构造函数只会让事情变得混乱,因为您永远不会使用它。

  • 如果这是公共API的一部分,请保留两者。如果您正在使用IoC,请确保您的IoC找到“最贪婪”的构造函数(具有最多参数的那个)。没有使用IoC但使用您的API的人将不需要构建整个依赖关系图表即可使用您的对象。

  • 如果您不使用IoC容器,但只想使用模拟进行单元测试,则保留无参数构造函数,并使贪婪的构造函数为内部构造函数。为您的单元测试程序集添加InternalsVisibleTo,以便它可以使用贪婪的构造函数。如果只是进行单元测试,则不需要额外的公共API接口。


这将成为公共API的一部分,因此我选择保留重载。仅因为默认值是合理的,我才考虑这个问题。我希望问题在IoC真空中,以避免为纯技术问题而妥协设计。 - Bryan Watts

4
我不会提供那个构造函数。这样做会使得通过调用 new TimeStamped 得到的实例使用了 new SystemTimestampProvider(),而你的 IoC 可能被配置为使用 OtherTimestampProvider(),这样最终导致你花大量时间来调试为什么得到了错误的时间戳。
如果你只提供第一个构造函数,你可以简单地查找 SystemTimestampProvider 的使用情况,以找出谁在(错误地)使用该提供程序而不是 IoC 配置的提供程序。

虽然在这种情况下没有使用IoC,但我假设大多数实现会在注册了提供程序的情况下进行注入,否则会使用默认构造函数。这意味着如果您没有覆盖ITimestampProvider,您将只获得SystemTimestampProvider,这正是预期的目的。你有什么想法? - Bryan Watts

3
一般来说,我不这么认为...这取决于你使用依赖注入的目的。当我将DI用于单元测试时,我通过在注入的实例为空时实例化被依赖对象的生产版本来做相同的事情(或者更多)...然后我有一个不带参数的重载方法并委托给有参数的方法...我在生产代码中使用无参数的方法,在单元测试方法中注入测试版本...
另一方面,如果你谈论IOC容器应用程序,则需要小心干涉配置设置,在不清楚的方式下干扰容器执行操作...
   public class EventsLogic
   { 
       private readonly IEventDAL ievtDal;
       public IEventDAL IEventDAL { get { return ievtDal; } }

       public EventsLogic(): this(null) {}
       public EventsLogic(IIEEWSDAL wsDal, IEventDAL evtDal)
       {
          ievtDal = evtDal ?? new EventDAL();
       }
    }

0

我尽量避免这种做法——在一些情况下这是个好的设计,但更多时候我发现它会导致我犯下一些难以解决的错误。

使用依赖注入容器(我使用StructureMap)来管理所有这些装配工作可以大大降低默认注入对象的需求——DI容器确保您始终获得可用的具体实例。

我仍然有一个地方倾向于使用您建议的构造函数,那就是我的单元测试中,但最近我使用伪造或模拟对象取得了更大的价值。

某些情况下,拥有默认的从属对象是正确和有用的设计,但总的来说,我认为您只是引入了不增加太多价值的紧密耦合。


0

这并不是有益的也不是有害的。它在美学上存在问题,因为您仅将 DI 限制为构造函数注入,而您的设计可能允许属性设置器注入。

另一个选择是实现一个返回默认实现的 getter:

public DateTime Timestamp
{
    get { return _timestampProvider??new SystemTimestampProvider(); }
    set { _timestampProvider = value; }
}

如果你担心在堆内存中创建太多对象,你也可以使用单例模式实现上述功能。


0

我的团队使用这种方法取得了很大的成功。我建议做一个改变:
将 _timestampProvider 设为只读。这将强制在构造时确定提供程序,并消除错误。

public class Timestamped
{
    private readonly ITimestampProvider _timestampProvider;

    public Timestamped(ITimestampProvider timestampProvider)
    {
        _timestampProvider = timestampProvider;
    }

    public Timestamped(): this(new SystemTimestampProvider())
    { }
}

话虽如此,我们始终关注新技术,包括 DI 框架。如果我们放弃这种技术并采用更好的替代方案,我会及时告知您。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接