依赖注入(DI)"友好"库

Pet*_*ete 227 c# dependency-injection inversion-of-control

我正在思考一个C#库的设计,它将有几个不同的高级函数.当然,这些高级功能将尽可能使用SOLID类设计原则来实现.因此,可能存在供消费者定期直接使用的类,以及作为那些更常见的"最终用户"类的依赖性的"支持类".

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

  • DI不可知 - 虽然为一个或两个常见的DI库(StructureMap,Ninject等)添加基本的"支持"似乎是合理的,但我希望消费者能够将该库与任何DI框架一起使用.
  • 非DI可用 - 如果库的使用者没有使用DI,那么库应该仍然尽可能容易使用,减少了用户为创建所有这些"不重要"的依赖关系而必须完成的工作量.他们想要使用的"真实"类.

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

思考?

Mar*_*ann 357

一旦你理解DI是关于模式和原则,而不是技术,这实际上很简单.

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

编程到接口,而不是实现

这个原则实际上是来自Design Patterns的引用(尽管来自内存),但它应该始终是你真正的目标.DI只是实现这一目标的一种手段.

应用好莱坞原则

DI术语中的好莱坞原则说:不要打电话给DI Container,它会打电话给你.

永远不要通过从代码中调用容器来直接询问依赖关系.使用构造函数注入隐式请求它.

使用构造函数注入

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

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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

使用构造函数注入注入的依赖关系往往是长期存在的,但有时您需要一个短期对象,或者基于仅在运行时知道的值来构造依赖关系.

有关更多信息,请参阅

仅在最后责任时刻撰写

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

更多细节在这里:

使用Facade简化

如果您认为生成的API对于新手用户来说过于复杂,您可以始终提供一些封装常见依赖关系组合的Facade类.

为了提供具有高度可发现性的灵活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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这将允许用户通过写入创建默认Foo

var foo = new MyFacade().CreateFoo();
Run Code Online (Sandbox Code Playgroud)

但是,它是非常可发现的,它可以提供自定义依赖,你可以写

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();
Run Code Online (Sandbox Code Playgroud)

如果您认为MyFacade类封装了许多不同的依赖项,我希望它能够清楚地表明如何提供正确的默认值,同时仍然可以发现可扩展性.


FWIW,在写完这个答案之后很久,我扩展了这里的概念,写了一篇关于DI友好图书馆的博客文章,以及关于DI友好框架的配套文章.

  • 好吧,这就是我们在Safewhere开发软件的方式,所以我们不分享你的经验...... (30认同)
  • 虽然这在纸面上听起来很棒,但根据我的经验,一旦你有很多内部组件以复杂的方式进行交互,你最终会有很多工厂需要管理,从而使维护更加困难.此外,工厂必须管理其创建的组件的生活方式,一旦您将库安装在真正的容器中,这将与容器自身的生活方式管理相冲突.工厂和外墙妨碍了真正的集装箱. (20认同)
  • 我认为外墙应该是手工编码的,因为它们代表了已知的(并且可能是常见的)组件组合.DI容器不是必需的,因为一切都可以手工连接(想想穷人的DI).回想一下,Facade只是API用户的可选便利类.高级用户可能仍希望绕过Facade并根据自己的喜好连接组件.他们可能想要使用他们自己的DI Contaier,所以我认为如果他们不打算使用特定的DI容器将是不可取的.可能但不可取 (18认同)
  • 这可能是我见过的最好的答案. (8认同)
  • 我还没有找到一个完成*全部*的项目. (4认同)

Aar*_*ght 39

术语"依赖注入"根本没有与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
    }
}
Run Code Online (Sandbox Code Playgroud)

你这样写:

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; }
}
Run Code Online (Sandbox Code Playgroud)

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

  1. 只要您认为可能需要更改实现,就依赖于接口而不是类;

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

这些都不是以任何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本身的一部分.

  • 容器的真正抽象是IServiceProvider,而不是IContainer. (2认同)
  • @Mauricio:你当然是对的,但是试着向那些从未使用过LWC系统的人解释为什么`IContainer`实际上不是一个段落中的容器.;) (2认同)