依赖注入(DI)“友好”库


230

我正在考虑C#库的设计,该库将具有几个不同的高级功能。当然,这些高级功能将尽可能使用SOLID类设计原则来实现。这样,可能会有旨在供消费者定期直接使用的类,以及作为那些更常见的“最终用户”类的依赖的“支持类”。

问题是,设计库的最佳方法是什么:

  • DI不可知-尽管为一个或两个常见的DI库(StructureMap,Ninject等)添加基本的“支持”似乎是合理的,但我希望消费者能够将库与任何DI框架一起使用。
  • 非DI可用-如果库的使用者不使用DI,则该库仍应尽可能易于使用,从而减少用户创建所有这些“不重要”依赖项而要做的工作量。他们想要使用的“真实”类。

我当前的想法是为常见的DI库提供一些“ DI注册模块”(例如,StructureMap注册表,Ninject模块)以及非DI的set或Factory类,并包含与这几个工厂的耦合。

有什么想法吗?


对断开的链接文章(SOLID)的新引用:butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

Answers:


360

一旦了解DI是关于模式和原理,而不是技术,这实际上很容易做到。

要以与DI容器无关的方式设计API,请遵循以下一般原则:

编程到接口,而不是实现

这个原则实际上是设计模式的引用(虽然来自内存),但是它应该始终是您的真正目标DI只是实现这一目标的一种手段

适用好莱坞原则

用DI的好莱坞原则说:不要叫DI容器,它会叫你

切勿通过在代码内调用容器直接请求依赖项。通过使用构造函数注入隐式地请求它。

使用构造函数注入

当您需要依赖项时,可通过构造函数静态地请求它:

public class Service : IService
{
    private readonly ISomeDependency dep;

    public Service(ISomeDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    public ISomeDependency Dependency
    {
        get { return this.dep; }
    }
}

注意Service类如何保证其不变性。创建实例后,由于Guard子句和readonly关键字的组合,因此可以确保依赖项可用。

如果需要短期对象,请使用Abstract Factory

使用构造函数注入注入的依赖关系通常是长期存在的,但是有时您需要一个短暂的对象,或者根据仅在运行时已知的值来构建依赖关系。

请参阅以获取更多信息。

仅在最后负责的时刻撰写

使对象解耦,直到最后。通常,您可以等待并在应用程序的入口点连接所有内容。这称为合成根

此处有更多详细信息:

使用立面简化

如果您觉得由此产生的API对于新手用户而言过于复杂,则可以始终提供一些封装常见依赖项组合的Facade类。

为了提供具有高度可发现性的灵活立面,您可以考虑提供Fluent Builders。像这样:

public class MyFacade
{
    private IMyDependency dep;

    public MyFacade()
    {
        this.dep = new DefaultDependency();
    }

    public MyFacade WithDependency(IMyDependency dependency)
    {
        this.dep = dependency;
        return this;
    }

    public Foo CreateFoo()
    {
        return new Foo(this.dep);
    }
}

这将允许用户通过编写默认Foo

var foo = new MyFacade().CreateFoo();

但是,很可能会发现可以提供自定义的依赖关系,并且您可以编写

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

如果您想象MyFacade类封装了许多不同的依赖项,那么我希望很清楚,它将如何提供适当的默认值,同时仍使可扩展性可发现。


FWIW,在写完这个答案很长时间之后,我扩展了本文的概念,并撰写了一篇有关DI友好库的较长博客文章,以及一篇关于DI友好框架的随笔文章。


21
虽然这听起来很不错,但以我的经验来看,一旦您有许多内部组件以复杂的方式进行交互,您最终将需要大量工厂进行管理,从而使维护变得更加困难。另外,工厂必须管理其创建的组件的生活方式,一旦将库安装在真实的容器中,这将与容器自己的生活方式管理相冲突。工厂和外墙妨碍了真正的容器。
Mauricio Scheffer 2010年

4
我还没有找到一个可以完成所有这些工作的项目。
Mauricio Scheffer 2010年

31
好吧,这就是我们在Safewhere开发软件的方式,所以我们不会分享您的经验……
Mark Seemann 2010年

19
我认为外墙应该是手工编码的,因为它们代表了已知的(并且可能是常见的)组件组合。DI容器不是必需的,因为所有东西都可以手工接线(想想可怜人的DI)。回想一下,Facade对于您的API用户而言只是一个可选的便利类。高级用户可能仍然希望绕过Facade并根据自己的喜好连接组件。他们可能想为此使用自己的DI Contaier,因此我认为如果他们不打算使用特定的DI容器,将是不明智的。可能但不建议
Mark Seemann 2010年

8
这可能是我在SO上见过的唯一最佳答案。
Nick Hodges

40

术语“依赖注入”与IoC容器完全没有任何关系,即使您倾向于看到它们在一起也是如此。它只是意味着,而不是像这样编写代码:

public class Service
{
    public Service()
    {
    }

    public void DoSomething()
    {
        SqlConnection connection = new SqlConnection("some connection string");
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        // Do something with connection and identity variables
    }
}

您可以这样写:

public class Service
{
    public Service(IDbConnection connection, IIdentity identity)
    {
        this.Connection = connection;
        this.Identity = identity;
    }

    public void DoSomething()
    {
        // Do something with Connection and Identity properties
    }

    protected IDbConnection Connection { get; private set; }
    protected IIdentity Identity { get; private set; }
}

也就是说,编写代码时您要做两件事:

  1. 每当您认为可能需要更改实现时,都依赖接口而不是类。

  2. 不用在类内部创建这些接口的实例,而是将它们作为构造函数参数传递(或者,可以将它们分配给公共属性;前者是构造函数注入,后者是属性注入)。

所有这些都不以任何DI库的存在为先决条件,并且如果没有一个DI库,它也不会使编写代码变得更加困难。

如果您正在寻找这样的示例,那么.NET Framework本身就是您的理想之选:

  • List<T>实施IList<T>。如果您将类设计为使用IList<T>(或IEnumerable<T>),则可以利用诸如惰性加载之类的概念,因为Linq to SQL,Linq to Entities和NHibernate都通常在后台进行属性注入。某些框架类实际上接受一个IList<T>作为构造函数的参数,例如BindingList<T>,用于多个数据绑定功能。

  • Linq to SQL和EF完全围绕IDbConnection和相关接口构建,可以通过公共构造函数传入。但是,您不需要使用它们。默认的构造函数在连接字符串位于配置文件中某个地方时可以正常工作。

  • 如果您曾经使用过WinForms组件,则需要处理“服务”,例如INameCreationServiceIExtenderProviderService。您甚至都不知道具体的课程什么。.NET实际上有其自己的IoC容器,IContainer该容器可用于此目的,并且Component该类具有作为GetService实际服务定位符的方法。当然,没有任何东西会阻止您在没有IContainer特定定位器的情况下使用任何或所有这些接口。服务本身仅与容器松散耦合。

  • WCF中的合同完全围绕接口构建。实际的具体服务类通常在配置文件中按名称引用,该文件本质上是DI。许多人没有意识到这一点,但是完全有可能将这个配置系统换成另一个IoC容器。也许更有趣的是,服务行为都是其所有实例IServiceBehavior,可以在以后添加。再次,您可以轻松地将其连接到IoC容器中,并让它选择相关的行为,但是该功能无需任何操作就可以完全使用。

等等等等。您可以在.NET中到处找到DI,通常情况下,它是如此无缝地完成,以至您甚至都不认为它是DI。

如果要设计具有DI功能的库以实现最大的可用性,那么最好的建议可能是使用轻量级容器提供您自己的默认IoC实现。 IContainer这是一个不错的选择,因为它是.NET Framework本身的一部分。


2
容器的真正抽象是IServiceProvider,而不是IContainer。
Mauricio Scheffer 2010年

2
@Mauricio:您当然是对的,但是请尝试向从未使用过LWC系统的人解释为什么IContainer容器实际上不在一个段落中。;)
Aaronaught

亚伦(Aaron),只是好奇为什么要使用私有集;而不只是将字段指定为只读?
jr3 2013年

@Jreeter:你的意思是为什么他们不private readonly田野?如果它们仅由声明类使用,那很好,但OP指定这是框架/库级代码,这意味着子类化。在这种情况下,您通常希望将重要的依赖项公开给子类。我本可以用显式的getter / setter 编写一个private readonly字段一个属性,但是...在示例中浪费了空间,并在实践中保留了更多代码,没有真正的好处。
Aaronaught

谢谢您的澄清!我假设孩子可以访问只读字段。
jr3 2013年

5

EDIT 2015:时间已经过去,我现在意识到这件事是一个巨大的错误。IoC容器非常糟糕,而DI是处理副作用的非常差的方法。实际上,应避免此处的所有答案(以及问题本身)。只需注意副作用,将其与纯代码分开,其他所有因素要么就位,要么无关紧要和不必要的复杂性。

原始答案如下:


在开发SolrNet时,我不得不面对同样的决定。我最初的目标是对DI友好且与容器无关,但是随着我添加越来越多的内部组件,内部工厂很快变得难以管理,并且生成的库变得不灵活。

我最终编写了自己的非常简单的嵌入式IoC容器,同时还提供了Windsor工具Ninject模块。将库与其他容器集成只是正确连接组件的问题,因此我可以轻松地将其与Autofac,Unity,StructureMap等集成。

这样做的缺点是我失去了仅new提供服务的能力。我还对CommonServiceLocator进行了依赖,可以避免这种依赖(以后可能会对其进行重构),以使嵌入式容器更易于实现。

在此博客文章中有更多详细信息。

MassTransit似乎依赖类似的东西。它具有一个IObjectBuilder接口,它实际上是CommonServiceLocator的IServiceLocator,具有多个方法,然后为每个容器(即NinjectObjectBuilder)和常规模块/设施(即MassTransitModule)实现此接口。然后,它依靠IObjectBuilder实例化它所需的内容。当然,这是一种有效的方法,但是我个人不太喜欢它,因为它实际上过多地绕过了容器,并将其用作服务定位器。

MonoRail也实现自己的容器,该容器实现良好的旧IServiceProvider。该容器在整个框架中通过暴露已知服务的接口使用。为了获得混凝土容器,它具有内置的服务提供商定位器。在温莎工厂指出该服务提供商定位温莎,使其成为选择的服务提供商。

底线:没有完美的解决方案。与任何设计决策一样,此问题要求在灵活性,可维护性和便利性之间取得平衡。


12
在尊重您的意见的同时,我发现您过于笼统的“这里的所有其他答案都已过时,应该避免”,这太幼稚了。如果从那以后我们学到了什么,那就是依赖注入是对语言限制的一种答案。在不发展或放弃当前语言的情况下,似乎我们将需要 DI来简化我们的体系结构并实现模块化。作为一个一般概念,IoC只是这些目标的结果,并且绝对是可取的目标。IoC 容器对于IoC或DI绝对不是必需的,其用途取决于我们制作的模块组成。
TNE

@tne您提到我“非常幼稚”是不受欢迎的。我已经在C#,VB.NET,Java中编写了没有DI或IoC的复杂应用程序(显然,根据您的描述判断,您认为“需要” IoC的语言)绝对不是关于语言限制的。这是关于概念上的限制。我邀请您学习有关函数式编程的知识,而不是求助于常规攻击和较差的论点。
Mauricio Scheffer

18
我提到发现你的说法很幼稚。这对您本人无能为力,也完全取决于我的观点。请不要被一个随机的陌生人侮辱。暗示我不了解所有相关的函数式编程方法也没有帮助;你不知道,那是错误的。我知道在那里知识渊博(迅速浏览了您的博客),这就是为什么我发现一个貌似博学的家伙会说诸如“我们不需要IoC”之类的消息令人讨厌。国际奥委会无处不在字面上发生在函数式编程(..)
TNE

4
以及一般的高级软件。实际上,功能语言(以及许多其他样式)确实证明了它们无需DI即可解决IoC,我想我们都同意这一点,这就是我想说的。如果您使用提到的语言在没有DI的情况下进行管理,您是如何做到的?我假设不使用ServiceLocator。如果应用功能性技术,最终将得到等效的闭包,即闭包注入的类(其中,闭包是闭包变量)。能怎样?(我真正好奇,因为我不喜欢DI无论是。)
TNE

1
IoC容器是全局可变字典,因此应避免使用参数化作为推理工具。每次您将函数作为值传递时,“不给我们打电话,我们都会给您打电话”中的IoC(概念)确实适用。而不是DI,而是使用ADT为您的域建模,然后编写解释器(参见Free)。
Mauricio Scheffer

1

我要做的是以不可知的方式设计我的库,以尽可能限制对容器的依赖。如果需要的话,这允许在DI容器上换出另一个容器。

然后将DI逻辑之上的层暴露给库的用户,以便他们可以使用通过接口选择的任何框架。这样,他们仍然可以使用您公开的DI功能,并且可以自由使用任何其他框架来实现自己的目的。

允许库的用户插入自己的DI框架对我来说有点不对劲,因为它大大增加了维护量。这样一来,与直接使用DI相比,这更像是插件环境。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.