关于C#:有没有其他方法可以替代注射私生子?(也称为通过默认构造函数进行的穷人注入)

Is there an alternative to bastard injection? (AKA poor man's injection via default constructor)

我通常会在一些情况下使用"混蛋注射法"。当我有一个"合适的"依赖注入构造函数时:

1
2
3
4
5
public class ThingMaker {
    ...
    public ThingMaker(IThingSource source){
        _source = source;
    }

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

1
2
3
    public ThingMaker() : this(new DefaultThingSource()) {}
    ...
}

这里明显的缺点是,这会创建对DefaultThingSource的静态依赖关系;理想情况下,不会有这种依赖关系,并且消费者总是会注入他们想要的任何源。然而,这太难使用了;消费者想要开发一个新的Thingmaker,开始做东西,然后几个月后当需要的时候再注入其他东西。在我看来,这只剩下几个选择:

  • 省略bastard构造函数;强制ThingMaker的使用者了解它的源代码,了解ThingMaker如何与它的源代码交互,查找或编写具体的类,然后在其构造函数调用中注入实例。
  • 省略bastard构造函数并提供一个单独的工厂、容器或其他引导类/方法;以某种方式使消费者理解他们不需要编写自己的ithingsource;强制Thingmaker的消费者查找和理解工厂或引导器并使用它。
  • 保留bastard构造函数,使消费者能够"新建"一个对象并使用它运行,并处理对defaultThingSource的可选静态依赖。
  • 孩子,3当然看起来很有吸引力。还有别的更好的选择吗?#1或2似乎不值得。


    据我所知,这个问题涉及到如何用一些适当的默认值公开松散耦合的API。在这种情况下,您可能有一个良好的本地默认值,在这种情况下,可以将依赖项视为可选的。处理可选依赖关系的一种方法是使用属性注入而不是构造函数注入——实际上,这是一种用于属性注入的海报场景。

    但是,bastard注入的真正危险是当默认值是外部默认值时,因为这意味着默认构造函数会将不需要的耦合拖到实现默认值的程序集上。不过,据我所知,预期违约将源自同一程序集,在这种情况下,我看不到任何特定的危险。

    在任何情况下,您也可以考虑一个外观,如我先前的回答之一所述:依赖注入(DI)"友好"库

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


    我的交易是在@brokinglass:

    1)唯一的构造函数是参数化的构造函数

    2)使用工厂方法创建ThingMaker并传入该默认源。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class ThingMaker {
      public ThingMaker(IThingSource source){
        _source = source;
      }

      public static ThingMaker CreateDefault() {
        return new ThingMaker(new DefaultThingSource());
      }
    }

    显然,这并不能消除您的依赖性,但它确实使我更清楚地认识到,这个对象具有依赖性,如果调用方愿意的话,可以深入研究这些依赖性。如果您愿意(CreateThingKerWithDefaultThingSource),可以使工厂方法更加明确,如果这有助于理解。我更喜欢这个方法来重写ithingsource工厂方法,因为它继续支持组合。当defaultThingSource被废弃时,您还可以添加一个新的工厂方法,并有一个清晰的方法来使用defaultThingSource查找所有代码并将其标记为要升级。

    你讨论了你的问题的可能性。在其他地方为方便或在类本身中为方便而设置的工厂类。唯一没有吸引力的选择是基于反射,进一步隐藏依赖关系。


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

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


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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class ThingMaker
    {
        private IThingSource _source;

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

        public ThingMaker() : this(ThingFactory.Current.CreateThingSource())
        {
        }
    }

    现在在工厂中创建默认实例并允许重写该方法:

    1
    2
    3
    4
    5
    6
    7
    public class ThingFactory
    {
        public virtual IThingSource CreateThingSource()
        {
            return new DefaultThingSource();
        }
    }

    更新:

    为什么使用两个构造函数:两个构造函数清楚地显示了类的用途。无参数构造器声明:只创建一个实例,类将执行它的所有职责。现在,第二个构造函数声明类依赖于IThingSource,并提供了一种使用不同于默认实现的实现的方法。

    为什么要使用工厂:1-规程:创建新实例不应该是此类职责的一部分,工厂类更合适。2-dry:假设在同一个API中,其他类也依赖于IThingsource并执行相同的操作。一旦返回IThingSource的工厂方法和API中的所有类自动开始使用新实例,则重写。

    我不认为ThingMaker与IThingSource的默认实现之间存在耦合问题,只要这个实现对整个API都有意义,而且您还提供了为测试和扩展目的重写这个依赖关系的方法。


    我投票赞成第3条。您将使您的生活——以及其他开发人员的生活——更加轻松。


    通常与这种类型的注入相关的示例通常非常简单:"在类B的默认构造函数中,使用new A()调用一个重载的构造函数,然后上路!"

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

    这种类型的注入向读者传达了这样的信息:您已经认识到了分离设计的好处,但是不愿意承诺它。我们都知道,当有人看到这个多汁的、简单的、低摩擦的默认构造函数时,不管从那时起程序有多严格,他们都会调用它。如果不读取默认构造函数的源代码,他们就无法理解程序的结构,而这在您刚分发程序集时不是一个选项。您可以记录连接字符串名称和应用程序设置键的约定,但在这一点上,代码并不独立,您将责任推给开发人员,以寻找正确的咒语。

    优化代码,使编写代码的人能够在不理解他们所说的是警笛之歌的情况下通过,这是一种反模式,最终导致在解开魔法时损失的时间比在最初的努力中节省的时间更多。要么脱钩,要么不脱钩;在每个图案中保持一只脚会降低两者的焦点。


    你对这种依赖的OO不洁感到不满意,但你并没有说它最终会导致什么麻烦。

    • ThingMaker是否以任何方式使用了不符合源的DefaultThingSource?不。
    • 是否会有一段时间强制退出无参数构造函数?因为此时您能够提供默认实现,所以不太可能。

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


    为了它的价值,我在Java中看到的所有标准代码都是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class ThingMaker  {
        private IThingSource  iThingSource;

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

    任何不需要DefaultThingSource对象的人都可以覆盖createIThingSource。(如果可能的话,对EDCOX1 1的调用将是在除构造函数之外的某个地方)。C ^不鼓励像Java那样重写,并且它可能不像Java中的那样明显,用户可以并且可能应该提供它们自己的IthIn源实现。(也不是很明显如何提供)我的猜测是3是可行的,但我想我会提到这一点。


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


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

  • 在API中创建一个助手,以允许API使用者使用其选择的IOC容器注册API中的"默认"接口实现。
  • 如果希望允许API使用者在不使用自己的IOC容器的情况下使用API,请在API中承载一个可选容器,该容器填充相同的"默认"实现。
  • 这里真正棘手的部分是决定何时激活容器2,最佳选择方法将在很大程度上取决于您预期的API消费者。


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

    这允许您在不使用参数或任何有关ThingSource的知识的情况下"新建"ThingMaker类,并且不直接依赖于DefaultThingSource。


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

    • 移除"混蛋建造师"
    • 在标准构造函数中,将源参数默认为空
    • 然后检查源是否为空,如果是这种情况,则将其分配为"new defaultThingSource()",否则将使用用户注入的任何内容。