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

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

问题是,设计库的最佳方式是什么?

  • DI不可知论者 - 尽管为一个或两个常见DI库(StructureMap,Ninject等)添加基本的“支持”似乎是合理的,但我希望消费者能够在任何DI框架中使用该库。
  • 非DI可用 - 如果图书馆的使用者没有使用DI,则图书馆应该尽可能便于使用,从而减少用户为创建所有这些“不重要”依赖项而必须执行的工作量他们想要使用的“真实”类。
  • 我目前的想法是为通用DI库(例如,一个StructureMap注册表,一个Ninject模块)以及一组或非工厂类的工厂类提供一些“DI注册模块”,并且包含与少数工厂的耦合。

    思考?


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

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

    编程到一个接口,而不是一个实现

    这个原则实际上是来自设计模式的引用(尽管如此),但它应该始终是您的真正目标 。 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; }
        }
    }
    

    注意Service类如何保证它的不变量。 一旦创建了实例,由于Guard子句和readonly关键字的组合,依赖关系将保证可用。

    如果您需要短暂的对象,请使用抽象工厂

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

    看到这个更多的信息。

    仅在最后责任时刻撰写

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

    更多细节在这里:

  • 我应该在哪里使用Ninject 2+进行注射(以及如何安排模块?)
  • 设计 - 在使用Windsor时应将物品登记在哪里
  • 简化使用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友好图书馆的更长博客文章,以及关于DI友好框架的配套文章。


    “依赖注入”一词与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; }
    }
    

    也就是说,当你编写你的代码时你做了两件事:

  • 只要您认为实施可能需要更改,请依靠接口而不是类;

  • 与其在类中创建这些接口的实例,不如在构造函数参数中传递它们(或者,它们可以分配给公共属性;前者是构造函数注入,后者是属性注入)。

  • 这一切都不是以任何DI库的存在为前提的,并且它没有真正使代码在没有一个的情况下编写更难。

    如果你正在寻找一个这样的例子,看看没有进一步比.NET框架本身:

  • List<T>实现IList<T> 。 如果您将类设计为使用IList<T> (或IEnumerable<T> ),则可以利用延迟加载等概念,因为Linq to SQL,Linq to Entities和NHibernate通常通过属性在幕后执行注射。 一些框架类实际上接受IList<T>作为构造函数参数,如BindingList<T> ,它用于多个数据绑定功能。

  • Linq到SQL和EF完全围绕IDbConnection和相关接口构建,可以通过公共构造函数传入。 不过,你不需要使用它们; 默认的构造函数工作得很好,连接字符串位于某个配置文件中。

  • 如果您曾在WinForms组件上工作,则处理“服务”,如INameCreationServiceIExtenderProviderService 。 你甚至不知道具体的类是什么。 .NET实际上有它自己的IoC容器IContainer ,它被用于此, Component类有一个GetService方法,它是实际的服务定位器。 当然,没有任何东西阻止你在没有IContainer或特定定位器的情况下使用任何或所有这些接口。 服务本身只与容器松散耦合。

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

  • 等等等等。 你可以在.NET中找到遍布整个地方的DI,它通常是无缝地完成的,所以你甚至不会把它想象成DI。

    如果你想设计你的启用DI的库以获得最大的可用性,那么最好的建议可能是使用一个轻量级容器提供你自己的默认IoC实现。 IContainer是一个很好的选择,因为它是.NET Framework本身的一部分。


    编辑2015年 :时间已过,我意识到现在这整个事情是一个巨大的错误。 IoC容器非常糟糕,DI是一种处理副作用的非常糟糕的方法。 实际上,所有的答案(和问题本身)都应该避免。 只需注意副作用,将它们与纯代码分开,其他所有内容都可能落入适当位置,或者是不相关且不必要的复杂性。

    原始答案如下:


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

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

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

    更多详细信息在这篇博文中。

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

    MonoRail也实现了自己的容器,它实现了很好的旧的IServiceProvider。 这个容器通过一个暴露知名服务的接口在整个框架中使用。 为了得到具体的容器,它有一个内置的服务提供者定位器。 温莎设施将此服务提供商定位器指向温莎,使其成为选定的服务提供商。

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

    链接地址: http://www.djcxy.com/p/13617.html

    上一篇: Dependency Inject (DI) "friendly" library

    下一篇: Dependency Injection vs Service Location