代码之家  ›  专栏  ›  技术社区  ›  Pete

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

  •  220
  • Pete  · 技术社区  · 16 年前

    我正在考虑一个C#库的设计,它将有几个不同的高级功能。当然,这些高级功能将使用 SOLID 尽可能多地遵循课堂设计原则。因此,可能会有供消费者定期直接使用的类,以及作为那些更常见的“最终用户”类的依赖项的“支持类”。

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

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

    思想?

    4 回复  |  直到 16 年前
        1
  •  364
  •   Community Mohan Dere    9 年前

    一旦您了解了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; }
        }
    }
    

    注意服务类如何保证其不变量。创建实例后,由于Guard子句和 readonly 关键词。

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

    this 了解更多信息。

    保持对象解耦,直到最后。通常,您可以等待并在应用程序的入口点连接所有内容。这就是所谓的 成分根 .

    简化外观的使用

    如果您觉得生成的API对于新手用户来说太复杂,您可以提供一些 Facade 封装公共依赖项组合的类。

    为了提供灵活的立面,具有高度的可发现性,你可以考虑提供流畅的建设者。大概是这样的:

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

    这将允许用户通过编写

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

    然而,很容易发现,可以提供自定义依赖项,并且您可以编写

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

    如果您想象MyFacade类封装了许多不同的依赖项,那么我希望您能够清楚地看到,它将如何在保持可扩展性的同时提供适当的默认值。


    FWIW,在写了这个答案很久之后,我扩展了这里的概念,并写了一篇关于 DI-Friendly Libraries ,以及一篇关于 DI-Friendly Frameworks .

        2
  •  42
  •   Aaronaught    16 年前

    术语“依赖注入”与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> . 如果您将类设计为使用 IEnumerable<T> ),您可以利用诸如延迟加载之类的概念,如Linq到SQL、Linq到实体和NHibernate,这些都是通过属性注入在后台完成的。有些框架类实际上接受 IList<T> 作为构造函数参数,例如 BindingList<T> ,用于多个数据绑定功能。

    • IDbConnection 以及相关接口,可以通过公共构造函数传入。不过,你不需要使用它们;默认构造函数在配置文件中的某个位置使用连接字符串时工作正常。

    • 如果您使用WinForms组件,您将处理“服务”,如 INameCreationService IExtenderProviderService . 你甚至不知道具体的类是什么 . .NET实际上有自己的IoC容器, IContainer Component 班上有一个 GetService 方法,它是实际的服务定位器。当然,没有什么可以阻止您在没有 IContainer 或者那个特定的定位器。服务本身仅与容器松散耦合。

    • WCF中的契约完全围绕接口构建。实际的具体服务类通常由配置文件中的名称引用,配置文件本质上是DI。许多人没有意识到这一点,但完全可以用另一个IoC容器替换这个配置系统。也许更有趣的是,服务行为都是 IServiceBehavior

    等等等等。你会在.NET中找到到处都是的DI,只是通常它是无缝完成的,以至于你甚至不认为它是DI。

    如果您希望设计支持DI的库以获得最大的可用性,那么最好的建议可能是使用轻量级容器提供您自己的默认IoC实现。

        3
  •  5
  •   Mauricio Scheffer    11 年前

    编辑2015

    原答覆如下:


    在开发过程中,我不得不面对同样的决定 SolrNet

    very simple embedded IoC container 同时也提供了 Windsor facility Ninject module . 将库与其他容器集成只是正确连接组件的问题,因此我可以轻松地将其与Autofac、Unity、StructureMap等集成。

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

    更多详细信息请参见本节 blog post

    MassTransit 似乎依赖于类似的东西。它有一个 IObjectBuilder NinjectObjectBuilder MassTransitModule . 然后它 relies on IObjectBuilder 举例说明它需要什么。当然,这是一种有效的方法,但我个人不太喜欢它,因为它实际上在容器中传递了太多信息,将其用作服务定位器。

    MonoRail 工具 its own container 同样,它实现了良好的旧功能 IServiceProvider . 此容器在整个框架中通过 an interface that exposes well-known services built-in service provider locator Windsor facility

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

        4
  •  1
  •   Igor Zevaka    16 年前

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