Is there an alternative to bastard injection? (AKA poor man's injection via default constructor)

I most commonly am tempted to use "bastard injection" in a few cases. When I have a "proper" dependency-injection constructor:

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

But then, for classes I am intending as public APIs (classes that other development teams will consume), I can never find a better option than to write a default "bastard" constructor with the most-likely needed dependency:

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

The obvious drawback here is that this creates a static dependency on DefaultThingSource; ideally, there would be no such dependency, and the consumer would always inject whatever IThingSource they wanted. However, this is too hard to use; consumers want to new up a ThingMaker and get to work making Things, then months later inject something else when the need arises. This leaves just a few options in my opinion:

  1. Omit the bastard constructor; force the consumer of ThingMaker to understand IThingSource, understand how ThingMaker interacts with IThingSource, find or write a concrete class, and then inject an instance in their constructor call.
  2. Omit the bastard constructor and provide a separate factory, container, or other bootstrapping class/method; somehow make the consumer understand that they don't need to write their own IThingSource; force the consumer of ThingMaker to find and understand the factory or bootstrapper and use it.
  3. Keep the bastard constructor, enabling the consumer to "new up" an object and run with it, and coping with the optional static dependency on DefaultThingSource.

Boy, #3 sure seems attractive. Is there another, better option? #1 or #2 just don't seem worth it.

11961 次浏览

一种替代方法是在 ThingMaker类中使用 工厂法CreateThingSource()为您创建依赖项。

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

我支持第三点,你将使你的生活——以及其他开发人员的生活——变得更加简单。

只是一个想法——也许更优雅一点,但遗憾的是,这并不能摆脱依赖性:

  • 删除“私生子构造函数”
  • 在标准构造函数中,将源参数默认设置为 null
  • 然后检查 source 是否为 null,如果是这种情况,则指定它为“ new DefaultThingSource ()”,否则不管使用者注入什么

建立一个内部工厂(库内部) ,将 defaultThingSource 映射到从缺省构造函数调用的 iThingSource。

这允许您“新建”ThingMaker 类,而不需要参数或任何有关 IThingSource 的知识,也不需要直接依赖于 DefaultThingSource。

您对这种依赖关系的 OO 杂质感到不满,但是您并没有真正说明它最终会导致什么麻烦。

  • ThingMaker 使用 DefaultThingSource 的方式与 IThingSource 不一致吗。
  • 有没有可能在某个时候,您会被迫退出无参数构造函数?由于此时您能够提供默认实现,因此不太可能。

I think the biggest problem here is the choice of name, not whether to use the technique.

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

  1. 在 API 中创建一个助手,允许 API 使用者从 API 中将“默认”接口实现注册到他们选择的 IoC 容器中。
  2. 如果希望允许 API 使用者在没有自己的 IoC 容器的情况下使用 API,那么在 API 中承载一个可选的容器,该容器填充了相同的“默认”实现。

这里真正棘手的部分是决定何时激活容器 # 2,最佳选择方法将在很大程度上取决于您预期的 API 使用者。

我支持选项1,只有一个扩展: 使 DefaultThingSource成为一个公共类。上面的措辞意味着 DefaultThingSource将对 API 的公共使用者隐藏,但是据我所知,没有理由不公开默认值。此外,您可以很容易地记录以下事实: 在特殊情况之外,new DefaultThingSource()总是可以传递给 ThingMaker

我的折衷方案是在@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 清楚地查找所有代码并将其标记为升级。

你的问题已经涵盖了所有的可能性。工厂类别的其他方便或一些方便内的类别本身。另一个不吸引人的选择是基于反射,这会进一步隐藏依赖关系。

不管怎样,我在 Java 中看到的所有标准代码都是这样的:

public class ThingMaker  {
private IThingSource  iThingSource;


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

Anybody who doesn't want a DefaultThingSource object can override createIThingSource. (If possible, the call to createIThingSource would be somewhere other than the constructor.) C# does not encourage overriding like Java does, and it might not be as obvious as it would be in Java that the users can and perhaps should provide their own IThingSource implementation. (Nor as obvious how to provide it.) My guess is that #3 is the way to go, but I thought I would mention this.

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

我将保留这两个构造函数,但是有一个工厂用于初始化。

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-DRY: 假设在相同的 API 中,其他类也依赖于 IThingSource,并且执行相同的操作。一旦返回 IThingSource 的工厂方法和 API 中的所有类自动开始使用新实例,重写。

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

据我所知,这个问题与如何公开具有适当缺省值的松散耦合 API 有关。在这种情况下,您可能有一个良好的 Local Default,在这种情况下,依赖关系可以被视为可选的。处理 可选的依赖关系的一种方法是使用 Property Injection而不是 构造函数注入——事实上,这是属性注入的海报场景。

然而,私生子注入的真正危险是当默认值是 外国违约时,因为这意味着缺省构造函数将沿着一个不希望的耦合拖动到实现默认值的程序集。然而,根据我对这个问题的理解,预期的默认值将来自同一个程序集,在这种情况下,我没有看到任何特定的危险。

In any case you might also consider a Facade as described in one of my earlier answers: 依赖注入(DI)“友好”库

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

通常与这种注入风格相关的例子往往非常简单: “在类 B的缺省构造函数中,用 new A()调用一个重载的构造函数,然后就可以开始了!”

事实上,依赖关系的构造通常非常复杂。例如,如果 B需要非类依赖项(如数据库连接或应用程序设置) ,该怎么办?然后将类 B绑定到 System.Configuration名称空间,增加了它的复杂性和耦合性,同时降低了它的一致性,所有这些都是为了编码细节,这些细节可以通过省略缺省构造函数来外化。

这种注入风格向读者表明,您已经认识到了解耦设计的好处,但是不愿意承诺它。我们都知道,当有人看到这个多汁、简单、低摩擦的缺省构造函数时,不管从那时起它让他们的程序变得多么僵硬,他们都会称之为。如果不读取程序缺省构造函数的源代码,他们就无法理解程序的结构,当你只是分发程序集时,这是不可能的。您可以记录连接字符串名称和应用程序设置键的约定,但是在这一点上,代码不能独立存在,您将责任推给开发人员来寻找正确的咒语。

优化代码,这样那些编写代码的人就可以在不理解他们所说的内容的情况下继续编写代码,这是一首诱人的歌曲,一种反模式,最终导致解开这个魔法所花费的时间比最初努力所节省的时间还要多。要么脱钩,要么不脱钩; 在每种模式中保留一只脚会减少两者的重点。