dependency-injection - 马克·西曼(Mark Seemann)关于 SCSS 注入(inject)的矛盾说法。需要澄清

标签 dependency-injection

我正在读他的书Dependency Injection in Net

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

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

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

2)Here(以及他的书中)他指出,一个类具有可选的依赖关系是可以的,只要该依赖关系的默认实现是一个好的Local Default即可:

但是在下一个article中,即使默认实现是Local Default,他似乎也完全反对具有可选的依赖项:

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)
g 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.



外部默认值是依赖关系的实现,该依赖关系用作默认值,并且在与其使用者不同的程序集中定义。因此,在使用外部默认值的情况下,使用者的程序集也将沿着依赖项的程序集拖动。

他是否还暗示“外部默认”使并行开发更加困难,而“本地默认”却没有?如果他是的话,那没有任何意义,因为我认为使并行开发变得困难的并不是因为消费者的组装很难引用依赖的组装,而是消费者类依赖于a的具体实现这一事实。依赖?

谢谢

最佳答案

由于此处存在许多问题,因此我将首先尝试就我对该主题的观点提供一个综合,然后根据该 Material 明确回答每个问题。

综合

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

Bastard注入(inject)液存在多个级别的问题:

  • 软件包依赖项
  • 封装
  • 易于使用

  • 最危险的问题与软件包依赖项有关。我试图通过引入“外部默认值”与“本地默认值”来使该概念更具可操作性。外部默认值的问题在于,它们会拖着硬耦合依赖项makes (de/re)composition impossibleAgile Principles, Patterns, and Practices是一个更好地处理包管理的好资源。

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

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

    尽管它保护了类的不变式,但问题在于,在这种情况下,null是可接受的输入值。这并非总是如此。在上面的示例中,LogManager.GetLogger("My")可能仅引入本地默认值。从此代码段中,我们无法得知这是否成立,但是为了争论起见,让我们现在假设这是正确的。如果默认的ILog确实是“本地默认值”,则MyConsumer的客户端可以传递null而不是ILog。请记住,封装是要使客户端易于使用对象而无需了解所有实现细节。这意味着客户可以看到的全部内容:

    public MyConsumer(ILog log)
    

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

    var mc = new MyConsumer(null);
    

    通过上述实现,不仅可以编译,而且可以在运行时运行。根据Postel's law,这是一件好事,对吗?

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

    考虑另一个具有必需依赖项的类;我们将其称为存储库,只是因为这是一个众所周知的(尽管过度使用)模式:

    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,则会出现运行时错误。

    尽管从技术上讲,这仍然是 SCSS 注入(inject),但I can live with this design用于本地默认设置;实际上,有时我会像这样设计API,因为它是许多语言中众所周知的习惯用法。

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

    Explicit is better than implicit

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

    另外,默认构造函数的存在还带来了一段代码要在Composition Root之外调用该默认构造函数的风险。如果发生这种情况,则说明您已经过早地将对象彼此耦合,一旦完成,就无法将它们与“合成根”内的对象分离。

    因此,使用普通构造函数注入(inject)的风险较小:

    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注入(inject)反模式吗?

    是的,从技术上讲,是的,但是后果不那么严重。最重要的是 SCSS 注入(inject)剂的描述,使您在遇到它时可以轻松识别它。

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

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

    从程序包依赖关系的 Angular 来看,您无需避免这种情况。他们是相对良性的。

    从使用 Angular 来看,我仍然倾向于避免使用它们,但这取决于我要构建的内容。
  • 如果我创建了许多人会使用的可重用库(例如OSS项目),我可能仍会选择“构造函数链接”,以便更轻松地开始使用API​​。
  • 如果我创建的类仅在特定的代码库中使用,则倾向于完全避免使用可选的依赖项,而是在“合成根目录”中显式地编写所有内容。

  • 3.他是否还暗示“外部默认”使并行开发更加困难,而“本地默认”则没有?

    不,我不知道如果您有默认设置,则必须先使用默认设置,然后才能使用。不管是本地还是外国都无所谓。

    关于dependency-injection - 马克·西曼(Mark Seemann)关于 SCSS 注入(inject)的矛盾说法。需要澄清,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21389283/

    相关文章:

    php - Symfony4 使用外部类库作为服务

    c# - 在 Startup.cs 中添加 DbContextOptions 而不是注册数据存储

    java - 当使用 new 运算符创建对象时,如何在 spring 中注入(inject)依赖项?

    javascript - 当它们属于同一个模块时,如何将 nestjs 服务注入(inject)另一个服务?

    dependency-injection - 用户输入依赖注入(inject)的最佳策略是什么?

    javascript - NodeJS 中的依赖注入(inject)模式

    dependency-injection - Symfony 服务严格参数

    unit-testing - 具有注入(inject)服务的单元测试抽象类

    javascript - 了解 AngularJS 中应用程序和测试中的注入(inject)依赖

    java - Autowiring 接口(interface)无需注释即可工作