Mark Seemann关于Bastard Injection的相互矛盾的陈述。

Mark Seemann's conflicting statements about Bastard Injection. Need some clarifications

我在网上读他的书《依赖注入》。

1)这里他说,Bastard Injection只有在我们使用Foreign Default的时候才出现。

但在他的书中,第148页的插图显示,当依赖项的默认实现是Foreign DefaultLocal Default时,Bastard Injection发生:

enter image description here

那么,当依赖项的默认实现是Local Default时,是否也会出现bastard注入反模式?

2)在这里(以及在他的书中),他注意到类具有可选的依赖关系是可以的,前提是该依赖关系的默认实现是一个好的Local Default

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

1
2
3
4
5
private readonly ILog log;
public MyConsumer(ILog log)
{
    this.log = log ??LogManager.GetLogger("My");
}

In terms of encapsulation, the main problem with such an approach is
that it seems like the MyConsumer class can't really make up its mind
whether or not it controls the creation of its log dependency. While
this is a simplified example, this could become a problem if the ILog
instance returned by LogManager wraps an unmanaged resource which
should be disposed when it's no longer needed.

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

3)第147页:

The main problem with Bastard Injection is its use of a FOREIGN
DEFAULT ... , we can no longer freely reuse the class because it drags
along a dependency we may not want. It also becomes more difficult to
do parallel development because the class depends strongly on its
DEPENDENCY.

外部默认值是用作默认值的依赖项的实现,它在与其使用者不同的程序集中定义。因此,对于外部默认值,使用者的程序集也将拖动依赖项的程序集。

他是否还暗示,外国违约会使并行开发变得更加困难,而地方违约不会?如果他是的话,那么这就没有意义了,因为我假设使并行开发变得困难的不是消费者的程序集很难引用依赖项的程序集,而是消费者类依赖依赖依赖于依赖项的具体实现这一事实?

谢谢


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

合成好的。

当我写这本书的时候,我首先尝试描述我在野外看到的模式和反模式。因此,书中的模式和反模式首先是描述性的,而且只是在较小程度上具有规定性。显然,将它们分为模式和反模式意味着一定程度的判断:)好的。

混蛋注射在多个层面上存在问题:好的。

  • 包依赖项
  • 包封
  • 易用性

最危险的问题与包依赖性有关。这是我试图通过引入"外国违约"和"本地违约"这两个术语来提高可操作性的概念。外部缺省的问题是它们会拖拽硬耦合的依赖项,这使得(de/re)组合成为不可能。更明确地处理包管理的一个好资源是敏捷原则、模式和实践。好的。

在封装级别上,这样的代码很难解释:好的。

1
2
3
4
5
private readonly ILog log;
public MyConsumer(ILog log)
{
    this.log = log ??LogManager.GetLogger("My");
}

虽然它保护了类的不变量,但问题是在这种情况下,null是一个可接受的输入值。情况并非总是如此。在上面的例子中,LogManager.GetLogger("My")可能只引入本地违约。从这段代码片段中,我们无法知道这是否是真的,但是为了便于讨论,现在我们假设这一点。如果违约的ILog确实是本地违约,那么MyConsumer的客户可以通过null而不是ILog。请记住,封装是为了让客户机在不了解所有实现细节的情况下更容易地使用对象。这意味着这就是客户看到的全部:好的。

1
public MyConsumer(ILog log)

在C语言(和类似语言)中,可以通过null而不是ILog,它将编译:好的。

1
var mc = new MyConsumer(null);

有了上述实现,不仅可以编译,而且还可以在运行时运行。根据波斯特尔定律,这是件好事,对吧?好的。

不幸的是,事实并非如此。好的。

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

1
2
3
4
5
6
7
8
private readonly IRepository repository;
public MyOtherConsumer(IRepository repository)
{
    if (repository == null)
        throw new ArgumentNullException("repository");

    this.repository = repository;
}

为了与封装保持一致,客户机只会看到:好的。

1
public MyOtherConsumer(IRepository repository)

根据以往的经验,程序员可能倾向于编写这样的代码:好的。

1
var moc = new MyOtherConsumer(null);

这仍在编译,但在运行时失败!好的。

如何区分这两个构造函数?好的。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
private readonly ILog log;

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

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

    this.log = log;
}

客户现在看到:好的。

1
2
public MyConsumer()
public MyConsumer(ILog log)

这与MyOtherConsumer是一致的,因为如果你试图通过null而不是ILog的话,你会得到一个运行时错误。好的。

虽然这在技术上仍然是混蛋注入,但我可以接受这种针对本地默认值的设计;事实上,我有时会这样设计API,因为它在许多语言中都是众所周知的习惯用法。好的。

在许多方面,这已经足够好了,但仍然违反了一个重要的设计原则:好的。

显式优于隐式好的。

虽然构造器链接使客户机可以将MyConsumer与默认的ILog一起使用,但没有简单的方法来确定ILog的默认实例是什么。有时候,这也很重要。好的。

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

因此,使用普通构造函数注入的风险较小:好的。

1
2
3
4
5
6
7
8
9
private readonly ILog log;

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

    this.log = log;
}

您仍然可以使用默认记录器编写MyConsumer:好的。

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

如果要使本地默认值更容易被发现,可以将其作为工厂公开,例如在MyConsumer类本身上:好的。

1
2
3
4
public static ILog CreateDefaultLog()
{
    return LogManager.GetLogger("My");
}

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

1。当依赖项的默认实现是本地默认值时,是否还会出现bastard注入反模式?好的。

是的,从技术上讲,确实如此,但后果不那么严重。混蛋注射首先是一种描述,当你遇到它时,它能让你很容易地识别出来。好的。

请注意,本书上面的插图描述了如何重构,而不是如何识别它。好的。

2。[应该]还应避免与本地默认值[…]的可选依赖关系?好的。

从包依赖的角度来看,您不需要避免这些问题;它们是相对良性的。好的。

从使用的角度来看,我仍然倾向于避免使用它们,但这取决于我在构建什么。好的。

  • 如果我创建了一个很多人都会使用的可重用库(例如OSS项目),我仍然可以选择构造函数链接,以便更容易地开始使用API。
  • 如果我创建的类只在特定的代码基中使用,那么我倾向于完全避免可选的依赖关系,而是在组合根中明确地组合所有内容。

三。他是否还暗示,外国违约会使并行开发变得更加困难,而地方违约不会?好的。

不,我没有。如果你有一个默认值,在你可以使用之前,默认值必须在适当的位置;不管是本地的还是国外的。好的。好啊。