有没有另类的混蛋注射?(AKA穷人通过默认构造函数注入)

Pat*_*ski 116 .net c# oop constructor dependency-injection

在一些情况下,我最常使用"混蛋注射".当我有一个"适当的"依赖注入构造函数时:

public class ThingMaker {
    ...
    public ThingMaker(IThingSource source){
        _source = source;
    }
Run Code Online (Sandbox Code Playgroud)

但是,对于我打算作为公共API(其他开发团队将使用的类)的类,我永远找不到比编写具有最可能需要的依赖项的默认"bastard"构造函数更好的选择:

    public ThingMaker() : this(new DefaultThingSource()) {} 
    ...
}
Run Code Online (Sandbox Code Playgroud)

这里明显的缺点是,这会对DefaultThingSource产生静态依赖; 理想情况下,没有这种依赖性,消费者总是会注入他们想要的任何IThingSource.但是,这太难用了; 消费者希望新建一个ThingMaker并开始制作物品,然后几个月后,在需要时注入其他东西.在我看来,这只留下了几个选项:

  1. 省略这个混蛋构造函数; 迫使ThingMaker的消费者理解IThingSource,了解ThingMaker如何与IThingSource交互,查找或编写具体类,然后在构造函数调用中注入实例.
  2. 省略bastard构造函数并提供单独的工厂,容器或其他引导类/方法; 以某种方式让消费者明白他们不需要编写自己的IThingSource; 迫使ThingMaker的消费者找到并理解工厂或引导程序并使用它.
  3. 保持bastard构造函数,使消费者能够"新建"一个对象并使用它运行,并处理DefaultThingSource上的可选静态依赖.

男孩,#3肯定看起来很有吸引力.还有另一种更好的选择吗?#1或#2似乎不值得.

Mar*_*ann 60

据我所知,这个问题涉及如何使用一些适当的默认值公开松散耦合的API.在这种情况下,您可能具有良好的本地默认值,在这种情况下,依赖关系可以视为可选.处理可选依赖项的一种方法是使用Property Injection而不是Constructor Injection - 事实上,这是属性注入的海报场景.

但是,Bastard Injection的真正危险是当默认值为Foreign Default时,因为这意味着默认构造函数拖拽与实现默认值的程序集之间的不合需要的耦合.但是,据我理解这个问题,预期的默认值将来自同一个程序集,在这种情况下,我没有看到任何特别的危险.

在任何情况下,您也可以考虑我之前的一个答案中描述的Facade:依赖注入(DI)"友好"库

顺便说一句,这里使用的术语基于我书中的模式语言.

  • 不能推荐@ MarkSeemann的书,足以解决DI和IoC的所有问题 (5认同)
  • 我同意在这种情况下重载的构造函数很好.我不同意Property Injection不足以宣传用户可以覆盖依赖关系,因为它是一个众所周知的模式,但我会批准重载的构造函数略微更"在你的脸上":) (4认同)
  • +1 DI友好的库响应是绝对必须在这个空间阅读,并使您能够从第一原则思考这些东西. (3认同)
  • 很好地考虑了本地默认值和外部默认值之间的区别.在这种情况下,它确实是一个本地默认值,尽管消费者可能有理由重写.在这种情况下,提供两个构造函数提示"使用默认值或注入您自己的"最简单.仅使用一个具有属性注入的默认构造函数不足以通告可注入的依赖项.你同意吗? (2认同)
  • @zomf 对库用户强制配置文件设置并不是一个特别友好的方法。它锁定客户端使用配置系统,这是不灵活的。如果您想从数据库而不是配置文件加载配置值怎么办?如果您想改变单元测试中的配置值怎么办?如果您想通过 UI 来配置配置值怎么办?...FWIW,我在[此处](http://blog.ploeh.dk/2014/05/19/di-Friendly-library)扩展了 DI 友好库建议,包括更多代码示例。 (2认同)

Ste*_*son 32

我的权衡取决于@BrokenGlass:

1)唯一构造函数是参数化构造函数

2)使用工厂方法创建ThingMaker并传入该默认源.

public class ThingMaker {
  public ThingMaker(IThingSource source){
    _source = source;
  }

  public static ThingMaker CreateDefault() {
    return new ThingMaker(new DefaultThingSource());
  }
}
Run Code Online (Sandbox Code Playgroud)

显然这并没有消除你的依赖性,但它确实让我更清楚的是,这个对象具有调用者可以深入研究的依赖关系.如果你喜欢(CreateThingMakerWithDefaultThingSource),你可以使工厂方法更加明确,如果这有助于理解.我更喜欢这个来覆盖IThingSource工厂方法,因为它继续支持组合.您还可以在DefaultThingSource废弃时添加新的工厂方法,并使用DefaultThingSource清楚地查找所有代码并将其标记为要升级.

您已经涵盖了问题的可能性.其他地方的工厂类为了方便或在课堂本身的一些便利.唯一另一个没有吸引力的选择是基于反射,甚至进一步隐藏依赖.

  • @Patrick在我看来,它使得依赖关系比使用默认构造函数更明确.设计类的方式似乎表明依赖是重要的(它是在构造函数中注入的)所以我会明确地说明.默认构造函数告诉我:"不要担心这种依赖,我只为测试或一次性案例创建了一个额外的构造函数".工厂方法告诉我"需要注意依赖,但这是你正在寻找的最可能的实现" (18认同)
  • 我仍然不明白为什么这比默认的构造函数更好 - 它有所有的缺点,没有额外的好处; 它只会使依赖关系更加混乱,任何读取代码的人. (3认同)
  • 这不应该是大写C的"CreateDefault"吗? (2认同)
  • 该解决方案并未明确如何使用此类.当你第一次使用类时,你首先看到的是构造函数,如果我只看到一个构造函数,我将假设我必须提供一个IThingSource实例才能使用ThingMaker.如果我是这样一个类的用户,我可能永远不会发现CreateDefault方法. (2认同)

Bro*_*ass 8

另一种方法是 CreateThingSource()ThingMaker类中使用工厂方法为您创建依赖项.

对于测试或者如果您确实需要其他类型,IThingSource则必须创建一个子类ThingMaker并重写CreateThingSource()以返回所需的具体类型.显然,如果您主要需要能够为测试注入依赖项,这种方法是值得的,但是对于大多数/所有其他目的而言,不需要另一个 IThingSource

  • 我想到了这一点,但它具有bastard构造函数选项的所有缺点,没有任何好处!对? (4认同)

Fer*_*ndo 6

我投票给#3.你将使你的生活 - 以及其他开发者的生活 - 变得更容易.

  • 你能解释*如何*?第三种方法避免了哪些问题? (6认同)

JCa*_*ico 6

如果你必须有一个"默认"依赖项,也称为穷人的依赖注入,那么你必须在某处初始化并"连接"依赖项.

我将保留两个构造函数,但只有初始化工厂.

public class ThingMaker
{
    private IThingSource _source;

    public ThingMaker(IThingSource source)
    {
        _source = source;
    }

    public ThingMaker() : this(ThingFactory.Current.CreateThingSource())
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

现在在工厂中创建默认实例并允许覆盖该方法:

public class ThingFactory
{
    public virtual IThingSource CreateThingSource()
    {
        return new DefaultThingSource();
    }
}
Run Code Online (Sandbox Code Playgroud)

更新:

为什么使用两个构造函数: 两个构造函数清楚地显示了如何使用该类.无参数构造函数声明:只需创建一个实例,该类将执行其所有职责.现在第二个构造函数声明该类依赖于IThingSource,并提供了一种使用不同于默认实现的实现的方法.

为什么使用工厂: 1-纪律:创建新实例不应该是此类职责的一部分,工厂类更合适.2-干:想象一下,在同一个API中,其他类也依赖于IThingSource并做同样的事情.一旦返回IThingSource的工厂方法和API中的所有类自动开始使用新实例,就覆盖.

我没有看到将ThingMaker与IThingSource的默认实现耦合的问题,只要这个实现对整个API有意义,并且您提供了为测试和扩展目的覆盖此依赖关系的方法.