除了混蛋注射,还有别的办法吗?(又名穷人's注入通过默认构造函数)

本文关键字:注入 构造函数 默认 混蛋 | 更新日期: 2023-09-27 18:02:19

在一些情况下,我通常会尝试使用"私生子注射"。当我有一个"合适的"依赖注入构造函数时:

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

但是,对于我打算作为公共api的类(其他开发团队将使用的类),我永远找不到比编写一个默认的"私生子"构造函数更好的选择,该构造函数具有最可能需要的依赖:

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

这里明显的缺点是,这会创建一个对DefaultThingSource的静态依赖;理想情况下,不存在这样的依赖,消费者总是会注入他们想要的任何IThingSource。然而,这太难用了;消费者想要安装一个新的ThingMaker,然后开始制造东西,几个月后,当需要的时候,再注入其他东西。在我看来,这只剩下几个选项:

  1. 省略私生子构造函数;强迫ThingMaker的消费者理解IThingSource,理解ThingMaker如何与IThingSource交互,找到或编写一个具体的类,然后在它们的构造函数调用中注入一个实例。
  2. 省略私生子构造函数,并提供一个单独的工厂、容器或其他引导类/方法;以某种方式让消费者明白他们不需要编写自己的IThingSource;强迫ThingMaker的消费者找到并理解工厂或引导程序并使用它。
  3. 保留私生子构造函数,使消费者能够"新建"一个对象并运行它,并处理对DefaultThingSource的可选静态依赖。

天哪,#3看起来很吸引人。有没有其他更好的选择?第一条或第二条似乎不值得。

除了混蛋注射,还有别的办法吗?(又名穷人's注入通过默认构造函数)

据我所知,这个问题涉及到如何使用一些适当的默认值公开松耦合API。在这种情况下,您可能有一个很好的本地默认值,在这种情况下,可以将依赖项视为可选的。处理可选依赖的一种方法是使用属性注入而不是构造函数注入——事实上,这是属性注入的海报场景。然而,私生子注入的真正危险是当默认值是Foreign default 时,因为这意味着默认构造函数将不希望的耦合拖到实现默认值的程序集上。然而,根据我对这个问题的理解,预期的默认值将来自同一个程序集中,在这种情况下,我没有看到任何特别的危险。

在任何情况下,您也可以考虑像我之前的一个答案中所描述的Facade:依赖注入(DI) "友好"图书馆

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

我的权衡是@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的所有代码,并将其标记为升级。

你的问题涵盖了所有的可能性。为了方便,在其他地方创建工厂类,或者在类本身中创建工厂类。另一个没有吸引力的选择是基于反射的,它进一步隐藏了依赖关系。

另一种选择是在您的ThingMaker类中有一个工厂方法CreateThingSource(),它为您创建依赖项。

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

我投票选择#3。您将使您的生活——以及其他开发人员的生活——变得更加轻松。

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

我将保留两个构造函数,但有一个工厂只是为了初始化。

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有意义,并且你提供了覆盖这个依赖的方法来进行测试和扩展。

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

  • 是ThingMaker使用DefaultThingSource在任何方式,不符合IThingSource?没有。
  • 是否会有一个时候,你将被迫退出无参数构造函数?因为此时您可以提供默认实现,所以不太可能。

我认为这里最大的问题是名称的选择,而不是是否使用该技术。

通常与这种注入风格相关的示例通常非常简单:"在B类的默认构造函数中,使用new A()调用重载构造函数,然后继续工作!"

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

这种注入风格告诉读者,您已经认识到解耦设计的好处,但不愿意采用它。我们都知道,当有人看到这个有趣的、简单的、低摩擦的默认构造函数时,他们会调用它,不管它从那时起使他们的程序变得多么严格。如果不阅读默认构造函数的源代码,它们就无法理解程序的结构,当您只是分发程序集时,这是不可能的。你可以记录连接字符串名称和应用设置键的约定,但此时代码不能独立存在,你把寻找正确咒语的责任交给了开发人员。

优化代码,使编写代码的人可以不理解他们在说什么,这是一种反模式,最终导致更多的时间浪费在解开魔法上,而不是最初努力节省的时间。要么分离,要么不分离;在每个模式中都保持一个脚会减少两者的焦点。

无论如何,我在Java中看到的所有标准代码都是这样做的:

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

任何不想要DefaultThingSource对象的人都可以重写createIThingSource。(如果可能的话,对createIThingSource的调用将在构造函数之外的其他地方。)c#不像Java那样鼓励重写,而且它可能不像在Java中那样明显,用户可以并且可能应该提供他们自己的IThingSource实现。(如何提供它也不那么明显。)我猜第三条是可行的,但我想我应该提一下这个。

只是一个想法-可能更优雅一点,但遗憾的是没有摆脱依赖:

  • 删除"杂种构造器"
  • 在标准构造函数中,你将源参数默认为null
  • 则检查source是否为空如果是这种情况则为其赋值为new DefaultThingSource()否则无论消费者注入的是什么

设置一个内部工厂(库内部),将DefaultThingSource映射到从默认构造函数调用的IThingSource。

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

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

  1. 在API中创建一个帮助器,允许API消费者从API中注册"默认"接口实现,并使用他们选择的IoC容器。
  2. 如果希望允许API消费者使用没有自己IoC容器的API,那么在API中托管一个可选的容器,该容器被填充为相同的"默认"实现。

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

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