首页 文章

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

提问于
浏览
215

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

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

  • DI不可知 - 虽然为一个或两个常见的DI库(StructureMap,Ninject等)添加基本"support"似乎是合理的,但我希望消费者能够将该库与任何DI框架一起使用 .

  • 非DI可用 - 如果库的使用者没有使用DI,则库应该仍然尽可能易于使用,从而减少用户创建所有这些"unimportant"依赖项所需的工作量,以便进入"real"他们想要使用的课程 .

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

思考?

4 回答

  • 340

    一旦你理解DI是关于 patterns and principles 而不是技术,这实际上很简单 .

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

    Program to an interface, not an implementation

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

    Apply the Hollywood Principle

    DI中的好莱坞原则说:Don 't call the DI Container, it' ll会打电话给你 .

    永远不要通过从代码中调用容器来直接询问依赖关系 . 使用 Constructor Injection 隐式请求它 .

    Use Constructor Injection

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

    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 关键字的组合,保证依赖性可用 .

    Use Abstract Factory if you need a short-lived object

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

    有关更多信息,请参见this .

    Compose only at the Last Responsible Moment

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

    更多细节在这里:

    Simplify using a 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

    术语“依赖注入”根本没有与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 Framework本身:

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

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

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

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

    等等等等 . 你会在.NET中找到DI,通常情况下它是如此无缝地完成,你甚至不认为它是DI .

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

  • 4

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

    原始答案如下:


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

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

    这样做的缺点是我失去了只提供服务的能力 . 我也依赖于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 locator . Windsor facility将此服务提供商定位器指向Windsor,使其成为选定的服务提供商 .

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

  • 37

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

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

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

相关问题