dependency injection(DI)“友好”库
我正在思考一个C#库的devise,这将有几个不同的高级function。 当然,这些高级函数将尽可能地使用SOLID类devise原则来实现。 因此,可能会有定期直接使用消费者的类别,以及那些更常见的“最终用户”类别的“支持类别”。
问题是,devise库的最佳方式是什么?
- DI不可知论 – 尽pipe为一个或两个常见DI库(StructureMap,Ninject等)添加基本的“支持”似乎是合理的,但我希望消费者能够使用任何DI框架的库。
- 非DI可用 – 如果图书馆的消费者没有使用DI,图书馆应该尽可能地容易使用,从而减less用户为创build所有这些“不重要”的依赖而必须做的工作量他们想要使用的“真实”类。
我目前的想法是为通用的DI库(例如,一个StructureMapregistry,一个Ninject模块)以及一个非DI的集合类或工厂类提供一些“DI注册模块”,并且包含与这些less数工厂的耦合。
思考?
一旦你了解DI是关于模式和原则 ,而不是技术,这实际上很简单。
要以DI容器不可知的方式deviseAPI,请遵循以下一般原则:
编程到一个接口,而不是一个实现
这个原则实际上是devise模式中的一个引用(尽pipe如此),但它应该始终是您的真正目标 。 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类如何保证不变式。 一旦创build了一个实例,由于Guard子句和readonly
关键字的组合,依赖关系保证可用。
如果您需要短暂的对象,请使用抽象工厂
注入构造器注入的依赖往往是长期的,但有时你需要一个短暂的对象,或基于只在运行时已知的值来构造依赖。
看到这个更多的信息。
仅在最后责任时刻撰写
保持对象解耦,直到最后。 通常情况下,您可以在应用程序的入口点等待和连接所有东西。 这被称为组合根 。
更多细节在这里:
- 我应该在哪里做Ninject 2+注射(以及如何安排我的模块?)
- devise – 在使用温莎时应将物品登记在哪里
简化使用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); } }
这将允许用户通过书写来创build默认的Foo
var foo = new MyFacade().CreateFoo();
但是,如果能提供一个自定义的依赖关系,那么你就可以写出来
var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();
如果您认为MyFacade类封装了很多不同的依赖关系,我希望能够清楚地知道它如何提供正确的默认值,同时还能使可扩展性成为可能。
FWIW,在写了这个答案很长一段时间之后,我对这里的概念进行了扩展,并且写了一篇关于DI-Friendly Libraries的更长的博客文章,以及一个关于DI友好框架的同伴文章。
“dependency injection”一词与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; } }
也就是说,当你编写你的代码时你做了两件事情:
-
依靠接口而不是类,只要你认为实现可能需要改变;
-
不要在类中创build这些接口的实例,而是将它们作为构造函数parameter passing(也可以将它们分配给公共属性;前者是构造函数注入 ,后者是属性注入 )。
这一切都不是以任何DI库的存在为前提的,也不会使代码变得更加困难。
如果你正在寻找一个这样的例子,看看没有进一步比.NET框架本身:
-
List<T>
实现IList<T>
。 如果你devise你的类使用IList<T>
(或IEnumerable<T>
),你可以利用延迟加载的概念,因为Linq to SQL,Linq to Entities和NHibernate都是幕后操作,通常是通过属性注射。 一些框架类实际上接受一个IList<T>
作为构造函数参数,比如BindingList<T>
,它用于多个数据绑定特性。 -
Linq到SQL和EF完全围绕
IDbConnection
和相关接口构build,可以通过公共构造函数传入。 不过,你不需要使用它们; 默认的构造函数工作得很好,连接string位于某个configuration文件中。 -
如果你使用WinForms组件处理“服务”,如
INameCreationService
或IExtenderProviderService
。 你甚至不知道具体的类是什么。 .NET实际上有它自己的IoC容器IContainer
,它被用于此,而Component
类有一个GetService
方法,它是实际的服务定位器。 当然,没有任何东西可以阻止你在没有IContainer
或特定定位器的情况下使用任何或所有这些接口。 服务本身只与容器松散耦合。 -
WCF中的契约完全围绕接口构build。 实际的具体服务类通常是在一个configuration文件中用名字来引用的,这个configuration文件本质上是DI。 很多人没有意识到这一点,但完全可以将这个configuration系统换成另一个IoC容器。 也许更有趣的是,服务行为都是以后可以添加的
IServiceBehavior
实例。 再次,你可以很容易地把它连接到一个IoC容器,并且可以select相关的行为,但是这个特性是完全没有用的。
等等等等。 你可以在.NET中find所有的DI,通常它是无缝地完成的,所以你甚至不用把它想成DI。
如果你想devise你的启用DI的库以获得最大的可用性,那么最好的build议可能是使用一个轻量级的容器提供你自己的默认IoC实现。 IContainer
是一个很好的select,因为它是.NET Framework本身的一部分。
编辑2015年 :时间已经过去,我意识到现在这整个事情是一个巨大的错误。 IoC容器非常糟糕,DI是一种处理副作用的非常糟糕的方法。 实际上,所有的答案(和问题本身)都应该避免。 只要意识到副作用,将它们从纯代码中分离出来,其他一切都落在原地,或者是不相关和不必要的复杂性。
原始答案如下:
在开发SolrNet时,我不得不面对同样的决定。 我开始的目标是DI友好和容器不可知论,但随着我添加越来越多的内部组件,内部工厂迅速变得难以pipe理,并且由此产生的库不灵活。
我最终编写了自己的非常简单的embedded式IoC容器,同时还提供了一个Windsor工具和一个Ninject模块 。 将库与其他容器集成在一起就是正确连接组件的问题,所以我可以轻松地将它与Autofac,Unity,StructureMap等结合起来。
这样做的缺点是我失去了提升服务的能力。 我也对CommonServiceLocator进行了依赖,我可以避免这种依赖(我将来可能会重构它),以使embedded式容器更容易实现。
更多细节在这个博客文章 。
MassTransit似乎依赖于类似的东西。 它有一个IObjectBuilder接口,它实际上是CommonServiceLocator的IServiceLocator以及更多的方法,然后为每个容器实现这个接口,即NinjectObjectBuilder和一个常规模块/设施,即MassTransitModule 。 然后它依靠IObjectBuilder来实例化它需要的东西。 这当然是一个有效的方法,但是我个人并不喜欢它,因为它实际上是绕过容器过多地使用它作为服务定位器。
MonoRail也实现了自己的容器 ,它实现了老的IServiceProvider 。 这个容器在整个框架中通过一个暴露知名服务的接口使用 。 为了得到具体的容器,它有一个内置的服务提供商定位器 。 温莎工厂将这个服务提供商定位器指向温莎,使其成为选定的服务提供商。
底线:没有完美的解决scheme。 与任何devise决定一样,这个问题需要在灵活性,可维护性和便利性之间取得平衡。
我会做的是devise我的图书馆在DI容器不可知的方式来尽可能限制对容器的依赖。 如果需要的话,这允许在DI容器上换出另一个。
然后将DI逻辑上方的图层展示给库的用户,以便他们可以使用通过界面select的任何框架。 这样他们仍然可以使用您公开的DIfunction,并且可以自由使用任何其他框架来实现自己的目的。
允许库的用户插入自己的DI框架似乎有点不对,因为它大大增加了维护量。 这也变成了比直接DI更多的插件环境。