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

bck*_*rld 22 dependency-injection

我正在阅读他的书" 网上依赖注入".

1)他在这里Bastard Injection只有在我们使用时才会发生Foreign Default.

但在他的书中,第148页上的插图显示,Bastard Injection当依赖项的默认实现是Foreign Default或者Local Default:

在此输入图像描述

那么当依赖的默认实现是一个时,Bastard Injection反模式也会出现Local Default吗?

2)在这里(以及他的书中)他指出,如果一个类有一个可选的依赖,那么这个依赖的默认实现是好的Local Default:

但在下一篇文章中,他似乎反对拥有可选的依赖项,即使默认实现Local Default:

private readonly ILog log;
public MyConsumer(ILog log)
{
    this.log = log ??LogManager.GetLogger("My");
}
Run Code Online (Sandbox Code Playgroud)

就封装而言,这种方法的主要问题在于,看起来MyConsumer类无论是否控制其日志依赖的创建都无法真正决定.虽然这是一个简化的示例,但如果LogManager返回的ILog实例包装了一个非托管资源,这可能会成为问题,该资源应该在不再需要时处理掉.

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

3)Pg.147:

Bastard Injection的主要问题是它使用了FOREIGN DEFAULT ...,我们不能再自由地重用该类了,因为它拖拽了我们可能不想要的依赖.进行并行开发也变得更加困难,因为类很大程度上依赖于它的依赖性.

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

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

谢谢

Mar*_*ann 35

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

合成

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

Bastard Injection存在多个问题:

  • 包依赖
  • 封装
  • 便于使用

最危险的问题与包依赖性有关.这是我试图通过引入外部默认本地默认术语来使其更具可操作性的概念.外部默认值的问题在于它们拖拽了硬耦合的依赖关系,这使得(de/re)组合成为不可能.一个更明确地处理包管理的好资源是敏捷原则,模式和实践.

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

private readonly ILog log;
public MyConsumer(ILog log)
{
    this.log = log ??LogManager.GetLogger("My");
}
Run Code Online (Sandbox Code Playgroud)

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

public MyConsumer(ILog log)
Run Code Online (Sandbox Code Playgroud)

在C#(和类似语言)中,可以传递null而不是ILog,并且它将编译:

var mc = new MyConsumer(null);
Run Code Online (Sandbox Code Playgroud)

通过上面的实现,不仅可以编译,还可以在运行时使用.根据Postel的定律,这是件好事,对吧?

不幸的是,事实并非如此.

考虑具有所需依赖性的另一个类; 让我们把它称为存储库,因为这是一个众所周知的(虽然过度使用)模式:

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

    this.repository = repository;
}
Run Code Online (Sandbox Code Playgroud)

为了与封装保持一致,客户端只能看到:

public MyOtherConsumer(IRepository repository)
Run Code Online (Sandbox Code Playgroud)

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

var moc = new MyOtherConsumer(null);
Run Code Online (Sandbox Code Playgroud)

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

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

public MyConsumer(ILog log)
public MyOtherConsumer(IRepository repository)
Run Code Online (Sandbox Code Playgroud)

你不能,但目前,你有不一致的行为:在一种情况下,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;
}
Run Code Online (Sandbox Code Playgroud)

客户现在看到了这个:

public MyConsumer()
public MyConsumer(ILog log)
Run Code Online (Sandbox Code Playgroud)

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

虽然这在技术上仍然是Bastard Injection,但我可以将这种设计用于Local Defaults ; 事实上,我有时会设计这样的API,因为它是许多语言中众所周知的习语.

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

显式优于隐式

虽然构造函数链接使客户端能够使用MyConsumer默认值ILog,但是没有简单的方法来确定默认实例是什么ILog.有时,这也很重要.

此外,默认构造函数的存在暴露了一段代码将在组合根之外调用该默认构造函数的风险.如果发生这种情况,你就会过早地与对象相互耦合,一旦你完成了这些,你就无法将它们从组合根中分离出来.

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

private readonly ILog log;

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

    this.log = log;
}
Run Code Online (Sandbox Code Playgroud)

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

var mc = new MyConsumer(LogManager.GetLogger("My"));
Run Code Online (Sandbox Code Playgroud)

如果您想让Local Default更容易被发现,您可以将它作为Factory出现在某处,例如在MyConsumer类本身上:

public static ILog CreateDefaultLog()
{
    return LogManager.GetLogger("My");
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

请注意,从书上面的插图介绍如何重构从庶子注射液; 不是如何识别它.

2. [应]还应避免使用本地默认值的可选依赖项吗?

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

从使用角度来看,我仍然倾向于避免使用它们,但这取决于我正在构建的内容.

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

3.他是否也暗示外国违约使并行开发更加困难,而本地违约则不然?

不,我没有.如果您有默认值,则必须先使用默认值,然后才能使用; 无论是本地还是外国都没关系.

  • 我不认为有人会回答,更不用说作者了,所以这是一个很好的惊喜.好书,很棒的答案.非常感谢 (10认同)