【问题标题】:Is there an alternative to bastard injection? (AKA poor man's injection via default constructor)有没有替代混蛋注射的方法? (又名穷人通过默认构造函数注入)
【发布时间】:2011-10-07 16:48:30
【问题描述】:

在少数情况下,我通常很想使用“混蛋注入”。当我有一个“正确的”依赖注入构造函数时:

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

但是,对于我打算作为 公共 API 的类(其他开发团队将使用的类),我再也找不到比编写一个默认的“混蛋”构造函数更好的选择了- 可能需要的依赖:

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

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

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

男孩,#3 确实看起来很有吸引力。还有其他更好的选择吗? #1 或 #2 似乎不值得。

【问题讨论】:

  • 为什么对 DefaultThingSource 的依赖比对 IThingSource 的依赖更糟?选择#3。
  • 因为 DefaultThingSource 可能是一个非常具体的实现,它依赖于另一个程序集中的外部系统,几个月后很可能会过时; IThingSource 永远是正确的,永远不会绑定到特定的实现。
  • 显然我不知道 DefaultThingSource 有多复杂。但是,我敢打赌,如果你保持简单并且设计得很好,实际上你会发现你的代码不会像你担心的那样过时——当然还不足以证明所有这些心痛的合理性。
  • 这就是依赖反转良好的问题——它可能更灵活,设计更好,但对于新程序员来说并不容易使用。
  • 你可能还想看看这篇文章:lostechies.com/jimmybogard/2009/07/03/…

标签: c# .net oop dependency-injection constructor


【解决方案1】:

据我了解,这个问题与如何使用一些适当的默认值公开松散耦合的 API 有关。在这种情况下,你可能有一个很好的Local Default,这种情况下依赖可以被认为是可选的。处理 可选依赖项 的一种方法是使用 Property Injection 而不是 Constructor Injection - 事实上,这是 Property 的海报场景注射。

但是,Bastard Injection 的真正危险在于默认值是外国默认值,因为这意味着默认构造函数会拖累实现默认值的程序集的不良耦合。但是,据我了解这个问题,预期的默认值将源自同一个程序集,在这种情况下,我没有看到任何特别的危险。

在任何情况下,您也可以考虑我之前的答案之一中描述的 Facade:Dependency Inject (DI) "friendly" library

顺便说一句,这里使用的术语是基于来自my book 的模式语言。

【讨论】:

  • +1 对 DI 友好的库响应绝对是这个领域的必读之物,它将使您能够从第一原理开始思考这些东西。
  • 很好地考虑了本地默认值和外国默认值之间的区别。在这种情况下,它确实是一个本地默认值,尽管消费者很可能有理由覆盖它。在这种情况下,提供两个构造函数提示“使用默认值或注入您自己的”最简单。仅使用一个带有属性注入的默认构造函数不足以宣传可注入依赖项。你同意吗?
  • 我同意在这种情况下重载的构造函数很好。我不同意属性注入不足以宣传用户可以覆盖依赖项,因为它是一种众所周知的模式,但我承认重载的构造函数稍微“在你的脸上”:)跨度>
  • 不能推荐@MarkSeemann 的书来解决所有这些DI 和IoC 问题
  • @zomf 对库用户强制进行配置文件设置并不是一种特别友好的方法。它锁定客户端以使用不灵活的配置系统。如果您想从数据库而不是配置文件加载配置值怎么办?如果你想在单元测试中改变配置值怎么办?如果您想通过 UI 使配置值可配置怎么办? ...FWIW,我已经扩展了 DI 友好库推荐 here,包括更多代码示例。
【解决方案2】:

我的取舍是对@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 的代码并将其标记为升级。

您在问题中涵盖了各种可能性。其他地方的工厂类是为了方便或类本身的一些方便。唯一没有吸引力的选择是基于反射的,进一步隐藏了依赖关系。

【讨论】:

  • 我仍然不明白为什么这比默认构造函数更好——它有所有的缺点而没有额外的好处;它只会让任何阅读代码的人更加混淆依赖关系。
  • @Patrick 在我看来,它使依赖关系比使用默认构造函数更明确。类的设计方式似乎表明依赖关系很重要(它被注入到构造函数中),所以我会明确表示。默认构造函数告诉我:“不要担心这种依赖关系,我只创建了一个额外的构造函数用于测试或一次性案例”。工厂方法告诉我“需要注意一个依赖项,但这是您正在寻找的最有可能的实现”
  • 不应该是带有大写 C 的“CreateDefault”吗?
  • 此解决方案没有明确说明如何使用此类。第一次使用类时,您首先要看的是构造函数,如果我只看到一个构造函数,我会假设我必须提供 IThingSource 的实例才能使用 ThingMaker。如果我是这样一个类的用户,我可能永远不会发现 CreateDefault 方法。
  • @Patrick - 很公平。如果您想更多地强调依赖关系,这只是一种选择。在这些决定中总是有一个权衡,这实际上是一个你想把重点放在哪里的问题。如果 DefaultThingSource 是大多数调用者的主要选项,那么我会鼓励在默认构造函数中使用它。如果您希望调用者考虑提供自己的 IThingSource,那么我认为这是一个可以接受的中间立场。如果您想强制用户提供 IThingSource,那么我开始倾向于工厂类。
【解决方案3】:

另一种方法是在您的 ThingMaker 类中添加一个 factory method CreateThingSource() 来为您创建依赖项。

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

【讨论】:

  • 我想到了这个,但它具有混蛋构造函数选项的所有缺点,没有任何好处!对吗?
  • 同意大部分 - 我确实使用默认构造函数自己创建“默认”依赖项,并且没有发现任何问题。这是不同的,因为它允许您不必在 any 构造函数中具有依赖项 - 这样您就可以避免另一个您称之为“混蛋构造函数”的东西,从而使公共接口更易于解析。这只有在更改依赖项仅与类的可测试性相关时才有意义。
【解决方案4】:

我投票给#3。你会让你的生活——以及其他开发者的生活——变得更轻松。

【讨论】:

  • 你能解释一下如何吗?第三种方法避免了哪些问题?
  • #1 需要为消费者做额外的工作。 #2 需要对消费者和帕特里克进行额外的工作。 #1 和#2 都没有提供足够的好处来超过依赖 DefaultThingSource 的感知劣势。
  • 我认为@Mark Seemann 的答案最接近于在现实世界中提供好的设计,但这个答案也很简单也很好。 :)
【解决方案5】:

如果你必须有一个“默认”依赖,也称为穷人的依赖注入,那么你必须在某个地方初始化和“连接”依赖。

我将保留两个构造函数,但有一个工厂仅用于初始化。

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

【讨论】:

  • 那为什么还要有工厂呢?只需将逻辑放在 ThingMaker 默认构造函数中即可。
  • 这是必要的,如果您绝对必须有一个公共 ctor。如果你不在那条船上,这是个坏主意。你能在你的答案中说清楚吗,或者我觉得它应该被否决,因为它没有添加任何其他答案没有做的事情,并且鉴于它没有消除耦合问题,这是一个明显更糟糕的实现。
  • @Ruben Bartelink:我已经更新了解释设计选择的答案。
  • “创建 [自身] 的新实例不应该是此类职责的一部分。”真的吗?你不能对每个创建的每个班级都这么说吗?我认为工厂方法/类有它们的位置,但我不能总是将它们用于所有事情。
  • @Patrick:这是一个偏好问题。我个人尝试将有意义的类的新实例的创建隔离到我的 API 中的一个位置,因此工厂使用。此外,通过使用工厂,可以轻松地从中心位置修改默认的 IThingSource。正如我之前所说的个人喜好。
【解决方案6】:

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

  • ThingMaker 是否以任何不符合 IThingSource 的方式使用 DefaultThingSource?没有。
  • 是否会在某个时候迫使您停用无参数构造函数?由于您目前能够提供默认实现,因此不太可能。

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

【讨论】:

  • 我不喜欢对 DefaultThingSource 的静态依赖。
  • 你为什么不喜欢依赖?
  • 我同意你的看法,大卫。除了我不会使用“OO impurity”这个短语,因为依赖关系没有任何不纯。这只是一个判断是否拥有它。我同意拥有它不是问题。
  • @David B:我认为它不受欢迎,因为一旦用户开始使用 AwesomeNewThing,它仍然会对 DefaultThingSource 有一个过时的未使用依赖项。
  • 这种依赖关系不是公共知识,可以在不影响用户的情况下进行更改。
【解决方案7】:

通常与这种注入风格相关的示例通常非常简单:“在类B 的默认构造函数中,使用new A() 调用重载构造函数,然后就可以了!”

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

这种注入风格向读者传达了您已经认识到解耦设计的好处但不愿意投入使用。我们都知道,当有人看到这个多汁、简单、低摩擦的默认构造函数时,他们会调用它,无论从那时起它使他们的程序变得多么僵硬。如果不阅读该默认构造函数的源代码,他们就无法理解其程序的结构,而当您仅分发程序集时,这不是一个选项。您可以记录连接字符串名称和应用程序设置键的约定,但此时代码并不独立,您将责任推给开发人员寻找正确的咒语。

优化代码,这样编写代码的人就可以在不理解他们所说的情况下顺利进行,这是一首警笛歌,一种反模式,最终导致解开魔法所浪费的时间比最初努力节省的时间更多。要么解耦,要么不解耦;在每种模式中保持一只脚会降低两者的焦点。

【讨论】:

  • 对不起,你没有说服我。两个构造函数——一个使简单的事情变得容易,另一个使困难的事情成为可能。你想让我做的事情似乎使简单的事情变得困难。
  • @Patrick Szalapski:我的观点是 1)您的代码的使用者在没有源代码或文档的情况下无法理解他们的程序结构,2)并非每个依赖项的构造函数都很简单,以及 3)您的对象承担了更多的知识和责任,从而淡化了他们的注意力。如果这些观点不能说服你,那么什么都不会。以这种风格构建一个完整的系统,并自行评估它是否让事情变得更容易。
【解决方案8】:

不管怎样,我在 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 实现。 (也不明显如何提供它。)我的猜测是#3是要走的路,但我想我会提到这一点。

【讨论】:

  • +0:-.5:不应该在 ctor 中调用 virtuals(一般来说 OO 100% 肯定,Java 99% 肯定)。 -.5:不解决耦合问题 +1:该技术的一个有趣变化,可以帮助人们思考,即使您不会将其用作实际解决方案
  • @Ruben Bartelink:是的,应该避免构造函数中的虚拟调用:stackoverflow.com/questions/119506/…
  • 我将虚拟调用放在构造函数中,以使示例尽可能简单。但是,在这种情况下,是我把它放在那里的。 “我在 Java 中看到的标准代码”不会在构造函数中调用等效的 createIThingSource()!
【解决方案9】:

只是一个想法——也许更优雅一点,但遗憾的是并没有摆脱依赖:

  • 删除“混蛋构造函数”
  • 在标准构造函数中,您将源参数默认为 null
  • 然后检查源是否为空,如果是这种情况,则将其分配为“new DefaultThingSource()”,否则无论消费者注入什么

【讨论】:

  • 这对未来的开发者隐藏了更多的默认依赖,这是不可取的。
【解决方案10】:

有一个将 DefaultThingSource 映射到 IThingSource 的内部工厂(在您的库内部),该工厂从默认构造函数中调用。

这允许您在没有参数或任何 IThingSource 知识且不直接依赖 DefaultThingSource 的情况下“新建” ThingMaker 类。

【讨论】:

  • 这听起来像是选项#3,但只是分成了多个类。它并没有摆脱静态依赖。
  • ThingMaker 在使用工厂时不再静态依赖 DefaultThingSource。如果您现在有了 IThingSource 的新实现,您只需在一个地方更改映射,即您的工厂。
  • 是的,但是 ThingMaker 对工厂有静态依赖,而工厂对我们默认的 Thing 源有静态依赖。我们想完全摆脱静态依赖。
  • 啊,那么这是理论练习吗?因为在您的情况下,我看不到完全没有静态依赖项的实际优势。
【解决方案11】:

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

  1. 在 API 中创建一个帮助程序,以允许 API 使用者使用他们选择的 IoC 容器从 API 注册“默认”接口实现。
  2. 如果希望允许 API 使用者在没有自己的 IoC 容器的情况下使用 API,请在 API 中托管一个填充相同“默认”实现的可选容器。

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

【讨论】:

  • 天哪,听起来很复杂。
  • 第一次才疼。 ;) 不幸的是,如果有人认真尝试支持作为 API 消费者的 IoC 和非 IoC 用户的需求,我看不出有更好的方法......
  • 我感觉这会伤害到试图维护此代码的每一个人。
【解决方案12】:

我支持选项 #1,有一个扩展名:DefaultThingSource 设为公共类。您上面的措辞暗示 DefaultThingSource 将对 API 的公共消费者隐藏,但据我了解您的情况,没有理由不公开默认值。此外,您可以轻松记录以下事实:在特殊情况之外,new DefaultThingSource() 始终可以传递给 ThingMaker

【讨论】:

  • 我在想象 DefaultThingSource 确实是公开的,而不是内部的。
  • 是的,这可行,但有什么理由说明这是一个更好的解决方案吗?
猜你喜欢
  • 2011-10-29
  • 2014-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多