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

时间:2011-07-18 13:31:03

标签: c# .net oop dependency-injection constructor

我最常在少数情况下使用“混蛋注射”。当我有一个“适当的”依赖注入构造函数时:

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

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

    public ThingMaker() : this(new DefaultThingSource()) {} 
    ...
}

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

  1. 省略这个混蛋构造函数;强迫ThingMaker的消费者理解IThingSource,了解ThingMaker如何与IThingSource交互,查找或编写具体类,然后在构造函数调用中注入实例。
  2. 省略bastard构造函数并提供单独的工厂,容器或其他引导类/方法;以某种方式让消费者明白他们不需要编写自己的IThingSource;迫使ThingMaker的消费者找到并理解工厂或引导程序并使用它。
  3. 保留bastard构造函数,使消费者能够“新建”一个对象并与之一起运行,并处理对DefaultThingSource的可选静态依赖。
  4. 男孩,#3肯定看起来很有吸引力。还有另一种更好的选择吗? #1或#2似乎不值得。

12 个答案:

答案 0 :(得分:60)

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

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

在任何情况下,您都可以考虑我之前的一个答案中描述的Facade:Dependency Inject (DI) "friendly" library

顺便说一句,这里使用的术语基于my book的模式语言。

答案 1 :(得分:32)

我的权衡取决于@BrokenGlass:

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

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

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

  public static ThingMaker CreateDefault() {
    return new ThingMaker(new DefaultThingSource());
  }
}

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

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

答案 2 :(得分:8)

另一种方法是在CreateThingSource()课程中设置factory method ThingMaker,为您创建依赖关系。

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

答案 3 :(得分:6)

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

答案 4 :(得分:6)

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

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

public class ThingMaker
{
    private IThingSource _source;

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

    public ThingMaker() : this(ThingFactory.Current.CreateThingSource())
    {
    }
}

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

public class ThingFactory
{
    public virtual IThingSource CreateThingSource()
    {
        return new DefaultThingSource();
    }
}

更新

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

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

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

答案 5 :(得分:2)

你对这种依赖的OO杂质不满意,但你并没有真正说出它最终导致的麻烦。

  • ThingMaker是否以任何不符合IThingSource的方式使用DefaultThingSource?否。
  • 有没有时间你会被迫退出无参数构造函数?由于您此时可以提供默认实现,因此不太可能。

我认为这里最大的问题是名称的选择,而不是是否使用该技术。

答案 6 :(得分:2)

通常与这种注入方式相关的示例通常非常简单:“在类B的默认构造函数中,使用new A()调用重载的构造函数并继续使用!”

现实情况是,依赖关系构建起来往往非常复杂。例如,如果B需要非类依赖,如数据库连接或应用程序设置,该怎么办?然后,您将类B绑定到System.Configuration命名空间,增加其复杂性和耦合,同时降低其一致性,所有这些都可以通过省略默认构造函数来编码可以简单地外部化的细节。

这种注射方式向读者传达了您已经认识到分离设计的好处但却不愿意承诺它。我们都知道,当有人看到那个多汁,简单,低摩擦的默认构造函数时,无论从那时起如何制作程序,他们都会调用它。如果不读取默认构造函数的源代码,他们无法理解程序的结构,当您只分发程序集时,这不是一个选项。您可以记录连接字符串名称和应用程序设置密钥的约定,但此时代码本身并不存在,您将责任放在开发人员身上以寻找正确的咒语。

优化代码,以便编写代码的人可以在不理解他们所说的内容的情况下获得警报歌曲,这种反模式最终会导致在解开魔法时失去的时间比在初始努力中节省的时间更多。要么解耦,要么不解耦;在每种模式中保持一只脚都会减少两者的重点。

答案 7 :(得分:1)

对于它的价值,我在Java中看到的所有标准代码都是这样的:

public class ThingMaker  {
    private IThingSource  iThingSource;

    public ThingMaker()  {
        iThingSource = createIThingSource();
    }
    public virtual IThingSource createIThingSource()  {
        return new DefaultThingSource();
    }
}

任何不想要DefaultThingSource对象的人都可以覆盖createIThingSource。 (如果可能的话,对createIThingSource的调用将在构造函数之外的其他地方。)C#不鼓励像Java那样覆盖,并且它可能不像用户可以并且可能应该在Java中那样明显。提供自己的IThingSource实现。 (也不是很明显如何提供它。)我的猜测是#3是要走的路,但我想我会提到这一点。

答案 8 :(得分:0)

只是一个想法 - 也许更优雅,但遗憾的是没有摆脱依赖:

  • 删除“混蛋构造函数”
  • 在标准构造函数中,您将源参数默认为null
  • 然后你检查源是否为null,如果是这种情况,你可以为其分配“new DefaultThingSource()”,而不管消费者注入的是什么

答案 9 :(得分:0)

有一个内部工厂(库内部)将DefaultThingSource映射到IThingSource,后者是从默认构造函数调用的。

这允许您在没有参数或任何IThingSource知识的情况下“新建”ThingMaker类,并且不直接依赖于DefaultThingSource。

答案 10 :(得分:0)

对于真正的公共API,我通常使用两部分方法来处理这个问题:

  1. 在API中创建一个帮助程序,以允许API使用者使用其选择的IoC容器从API注册“默认”接口实现。
  2. 如果希望API使用者在没有自己的IoC容器的情况下使用API​​,请在API中托管一个可选容器,该容器填充相同的“默认”实现。
  3. 这里真正棘手的部分是决定何时激活容器#2,最佳选择方法将在很大程度上取决于您的API消费者。

答案 11 :(得分:0)

我支持选项#1,只有一个扩展名:使DefaultThingSource成为公共类。您上面的措辞意味着DefaultThingSource将隐藏API的公共使用者,但据我了解您的情况,我们没有理由不公开默认值。此外,您可以轻松记录这样的事实:在特殊情况下,new DefaultThingSource()始终可以传递给ThingMaker