依赖注入(DI)“友好”库
我在思考一个C#库的设计,它会有几个不同的高级函数。 当然,这些高级函数将尽可能使用SOLID类设计原则来实现。 因此,可能会有一些旨在供消费者直接定期使用的类别,以及那些更常见的“最终用户”类别的“支持类别”。
问题是,设计库的最佳方式是什么?
我目前的想法是为通用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
关键字的组合,依赖关系将保证可用。
如果您需要短暂的对象,请使用抽象工厂
使用构造函数注入注入的依赖往往是长期存在的,但有时您需要一个短暂的对象,或者基于仅在运行时已知的值构建依赖项。
看到这个更多的信息。
仅在最后责任时刻撰写
保持对象解耦,直到最后。 通常,您可以等待并在应用程序的入口点连接所有东西。 这被称为组合根 。
更多细节在这里:
简化使用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组件上工作,则处理“服务”,如INameCreationService
或IExtenderProviderService
。 你甚至不知道具体的类是什么。 .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