Mark Seemann关于Bastard Injection的相互矛盾的陈述。需要一些澄清

时间:2014-01-27 18:57:14

标签: dependency-injection

我正在读他的书Dependency Injection in Net

1)Here他说Bastard Injection仅在我们使用Foreign Default时才会发生。

但在他的书中,第148页上的插图显示,当依赖项的默认实现为Bastard InjectionForeign Default时,会出现Local Default

enter image description here

当依赖的默认实现是Local Default时,Bastard Injection反模式也会发生吗?

2)Here(以及他的书中)他指出,只要默认实现,类就可以拥有可选依赖这种依赖是一个很好的Local Default

但在下一个article中他似乎反对拥有可选依赖项,即使默认实现Local Default:< / p>

private readonly ILog log;
public MyConsumer(ILog log)
{
    this.log = log ??LogManager.GetLogger("My");
}
     

在封装方面,这种方法的主要问题是   似乎MyConsumer课程无法真正下定决心   它是否控制其日志依赖项的创建。而   这是一个简化的例子,如果是ILog,这可能会成为一个问题   LogManager返回的实例包装了一个非托管资源   应该在不再需要时处理。

当依赖的默认实现是本地的时,他在上述摘录中的参数是否也有效?如果是这样,那么还应该避免使用具有本地默认值的可选依赖项吗?

3) PG。 147:

  

Bastard Injection的主要问题是使用FOREIGN   DEFAULT ...,我们不能再自由地重用该类,因为它拖延了   沿着我们可能不想要的依赖。它也变得更加困难   并行开发,因为类强烈依赖于它   特性的研究。

Foreign Default是一个依赖项的实现,它被用作默认值,并且在与其使用者不同的程序集中定义。因此,对于Foreign Default,使用者的程序集也会依赖于依赖项的程序集。

他是否也暗示Foreign Default使并行开发更加困难,而Local Default则不然?如果他是,那么这没有意义,因为我认为使并行开发变得困难的原因并不是消费者的集合很难引用依赖的汇编,而是消费者阶层依赖于一个具体实现的事实。依赖

感谢

1 个答案:

答案 0 :(得分:35)

由于这里有很多问题,我将首先尝试对我对该主题的看法进行综合,然后根据这些材料明确回答每个问题。

<强>合成

当我写the book时,我首先试图描述我在野外目睹的模式和反模式。因此,本书中的模式和反模式首先是描述性的,并且仅在较小程度上规范性。显然,将它们分为模式反模式意味着一定程度的判断:)

Bastard Injection存在多个层面的问题:

  • 包依赖
  • 封装
  • 易用性

最危险的问题与包依赖有关。这是我试图通过引入术语外部默认本地默认来实现更多可操作性的概念。 外部默认值的问题在于它们拖动了硬连接的依赖关系makes (de/re)composition impossible。一个更明确地处理包管理的好资源是Agile Principles, Patterns, and Practices

封装的级别上,这样的代码很难理解:

private readonly ILog log;
public MyConsumer(ILog log)
{
    this.log = log ??LogManager.GetLogger("My");
}

虽然它保护了班级&#39;不变量,问题是在这种情况下null是可接受的输入值。这并非总是如此。在上面的示例中,LogManager.GetLogger("My")可能只会引入本地默认。从这段代码片段中,我们无法知道这是否属实,但为了论证,让我们暂时假设这一点。如果默认ILog确实是本地默认,则MyConsumer的客户可以传入null而不是ILog。请记住,封装是为了让客户端在不了解所有实现细节的情况下轻松使用对象。这意味着这是客户看到的全部内容:

public MyConsumer(ILog log)

在C#(和类似语言)中,可以通过null代替ILog,并且可以编译:

var mc = new MyConsumer(null);

通过上面的实现,不仅可以编译,还可以在运行时使用。根据{{​​3}},这是件好事,对吗?

不幸的是,它不是。

考虑具有必需依赖关系的另一个类;我们称之为 Repository ,因为这是一个众所周知的(虽然过度使用)模式:

private readonly IRepository repository;
public MyOtherConsumer(IRepository repository)
{
    if (repository == null)
        throw new ArgumentNullException("repository");

    this.repository = repository;
}

与封装一致,客户端只能看到:

public MyOtherConsumer(IRepository repository)

根据以往的经验,程序员可能倾向于编写如下代码:

var moc = new MyOtherConsumer(null);

这仍然可以编译,但在运行时失败!

你如何区分这两个构造函数?

public MyConsumer(ILog log)
public MyOtherConsumer(IRepository repository)

您不能,但目前,您的行为不一致:在一种情况下,null是有效参数,但在另一种情况下,null将导致运行时异常。这将降低每个客户端程序员在API中的信任度。 一致是更好的前进方式。

为了使MyConsumer 这样的课程更容易使用,您必须保持一致。这就是为什么接受null是一个坏主意的原因。更好的方法是使用构造函数链接:

private readonly ILog log;

public MyConsumer() : this(LogManager.GetLogger("My")) {}

public MyConsumer(ILog log)
{
    if (log == null)
        throw new ArgumentNullException("log");

    this.log = log;
}

客户现在看到了这个:

public MyConsumer()
public MyConsumer(ILog log)

这与MyOtherConsumer一致,因为如果您尝试传递null而不是ILog,则会出现运行时错误。

虽然技术上仍然是Bastard Injection,Postel's law用于本地默认值;事实上,我有时会设计这样的API,因为它是许多语言中众所周知的习语。

出于许多目的,这已经足够了,但仍然违反了一个重要的设计原则:

I can live with this design

虽然构造函数链接使客户端能够将MyConsumer与默认ILog一起使用,但是没有简单的方法可以找出ILog的默认实例。有时,这也很重要。

此外,默认构造函数的存在暴露了一段代码将调用Explicit is better than implicit之外的默认构造函数的风险。如果发生这种情况,您就会过早地与对象相互耦合,并且一旦您完成了这些操作,就无法将它们从组合根中解耦。

因此,使用普通构造函数注入所涉及的风险较小:

private readonly ILog log;

public MyConsumer(ILog log)
{
    if (log == null)
        throw new ArgumentNullException("log");

    this.log = log;
}

您仍然可以使用默认记录器撰写MyConsumer

var mc = new MyConsumer(LogManager.GetLogger("My"));

如果您想让本地默认更容易被发现,您可以将其公开为某处的工厂,例如:在MyConsumer类本身:

public static ILog CreateDefaultLog()
{
    return LogManager.GetLogger("My");
}

所有这些都为回答这个问题中的具体子问题奠定了基础。

<强> 1。当依赖项的默认实现是本地默认值时,是否还会出现Bastard Injection反模式?

是的,从技术上讲,确实如此,但后果不那么严重。 Bastard Injection 首先是一个描述,使您能够在遇到它时轻松识别它。

请注意,本书上面的插图描述了如何从Bastard Injection中重构;不是如何识别它。

<强> 2。 [应该]还可以避免使用本地默认值的可选依赖项吗?

从包依赖的角度来看,你不需要避免这些;他们相对温和。

从使用角度来看,我仍然倾向于避开它们,但这取决于我所构建的内容。

  • 如果我创建一个很多人会使用的可重用库(例如OSS项目),我仍然可以选择Constructor Chaining,以便更容易开始使用API​​。
  • 如果我只创建一个仅在特定代码库中使用的类,我倾向于完全避免使用可选的依赖项,而是在Composition Root中组成一切明确的内容。

第3。他是否也暗示外国违约使并行开发更加困难,而本地默认不会?

不,我不是。如果您有默认值,则必须先使用默认值,然后才能使用;它是本地还是外国并不重要。