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

时间:2010-01-12 00:20:37

标签: c# dependency-injection inversion-of-control

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

问题是,设计图书馆的最佳方式是:

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

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

思想?

4 个答案:

答案 0 :(得分:354)

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

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

编程到界面,而不是实现

这个原则实际上是来自Design Patterns的引用(虽然来自记忆),但它应该始终是你的真正的目标 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

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

有关详细信息,请参阅this

仅在最后责任时刻撰写

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

此处有更多详情:

使用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);
    }
}

这将允许用户通过编写

来创建默认的Foo
var foo = new MyFacade().CreateFoo();
然而,如果可以提供自定义依赖项,那么它是可以发现的,你可以编写

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

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


FWIW,在写完这个答案之后很久,我扩展了这里的概念并写了一篇关于DI-Friendly Libraries的更长篇博文,以及一篇关于DI-Friendly Frameworks的配帖。

答案 1 :(得分:38)

术语“依赖注入”根本没有与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. 不是在类中创建这些接口的实例,而是将它们作为构造函数参数传递(或者,它们可以分配给公共属性;前者是构造函数注入,后者是< em>属性注入)。

  3. 这些都不是以任何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 :(得分:4)

2015年编辑:时间过去了,我现在意识到这一切都是一个巨大的错误。 IoC容器很糟糕,DI是处理副作用的一种非常差的方法。实际上,这里的所有答案(以及问题本身)都应该避免。只需要注意副作用,将它们与纯粹的代码区分开来,其他一切都要么落实到位,要么就是无关紧要和不必要的复杂性。

原始答案如下:


在开发SolrNet时,我不得不面对同样的决定。我开始的目标是DI友好和容器不可知,但随着我添加越来越多的内部组件,内部工厂很快变得无法管理,结果库变得不灵活。

我最后编写了自己的very simple embedded IoC container,同时还提供了Windsor facilityNinject module。将库与其他容器集成只需要正确连接组件,因此我可以轻松地将它与Autofac,Unity,StructureMap等集成。

这样做的缺点是我失去了new服务的能力。我还依赖于CommonServiceLocator,我可以避免(我将来可能会重构)以使嵌入式容器更容易实现。

blog post中的详细信息。

MassTransit似乎依赖于类似的东西。它有一个IObjectBuilder接口,它实际上是CommonServiceLocator的IServiceLocator,有更多方法,然后它为每个容器实现这个,即NinjectObjectBuilder和常规模块/设施,即MassTransitModule。然后它relies on IObjectBuilder来实例化它需要的东西。这当然是一种有效的方法,但我个人并不喜欢它,因为它实际上过多地使用它作为服务定位器。

MonoRail也实现了its own container,它实现了良好的旧IServiceProvider。整个框架通过an interface that exposes well-known services使用此容器。要获得具体容器,它有一个built-in service provider locatorWindsor facility将此服务提供商定位器指向Windsor,使其成为选定的服务提供商。

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

答案 3 :(得分:1)

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

然后将DI逻辑上方的层暴露给库的用户,以便他们可以使用您通过界面选择的任何框架。通过这种方式,他们仍然可以使用您公开的DI功能,并且可以根据自己的目的自由使用任何其他框架。

允许库的用户插入他们自己的DI框架对我来说似乎有点不对,因为它大大增加了维护量。这也比直接DI更像是一个插件环境。