问题是,设计库的最佳方式是什么?
- DI Agnostic - 虽然对于一两个常见的DI库(StructureMap,Ninject等)添加基本的“支持”似乎是合理的,但我希望消费者能够使用任何DI框架来使用库。
- Non-DI usable - 如果库的消费者不使用DI,则该库仍应尽可能易于使用,减少用户必须做的所有这些“不重要”的依赖项的创建量,以便到达他们想要使用的“真正”类。
您有什么想法?
理解了DI是关于模式和原则而不是技术,实际上这很简单。
要以与DI容器无关的方式设计API,请遵循以下一般原则:
针对接口编程,而不是实现
这个原则实际上是摘自《设计模式》(尽管是从记忆中摘录的),但它应该始终是您的真正目标。 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类如何保证其不变式。一旦创建实例,由于Guard Clause和readonly
关键字的组合,依赖项保证可用。
如果需要短暂的对象,请使用抽象工厂
使用构造函数注入的依赖项往往具有长生命周期,但有时需要一个短生命周期的对象或根据仅在运行时已知的值构建依赖项。
请参见此处以获取更多信息。
仅在最后负责时构成
将对象保持解耦,直到最后才组合。通常,您可以等待并在应用程序的入口点中连接所有内容。这称为组合根。
更多详细信息请看:
使用Facade简化
如果您觉得为初学者用户提供的API变得太复杂,可以始终提供一些封装常用依赖项组合的Facade类。
要提供灵活的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);
}
}
这将允许用户通过编写以下内容来创建默认的Foo
var foo = new MyFacade().CreateFoo();
然而,很容易发现可以提供自定义依赖项,您可以编写
var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();
如果你想象一下MyFacade类封装了很多不同的依赖关系,希望现在清楚了如何提供适当的默认值,同时仍然使可扩展性易于发现。
顺便说一句,在撰写这篇答案后很久,我进一步阐述了这里的概念,并撰写了一篇更长的博客DI-Friendly Libraries,以及一篇关于DI-Friendly Frameworks的配套文章。
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; }
}
如果您认为实现可能需要更改,则应该依赖于接口而不是类;
在类内部不应创建这些接口的实例,而是将它们作为构造函数参数传递(或者将它们分配给公共属性;前者是构造函数注入,后者是属性注入)。
List<T>
实现了 IList<T>
。如果您设计您的类使用 IList<T>
(或者 IEnumerable<T>
),则可以利用懒加载等概念,如 Linq to SQL、Linq to Entities 和 NHibernate 在幕后执行的操作,通常通过属性注入实现。有些框架类实际上接受 IList<T>
作为构造函数参数,例如用于多个数据绑定特性的 BindingList<T>
。
Linq to SQL 和 EF 完全基于 IDbConnection
和相关接口构建,可以通过公共构造函数传入。不过,您也可以使用默认构造函数,并在某个配置文件中设置连接字符串。
如果您曾经使用过 WinForms 组件,则会涉及“服务”,例如 INameCreationService
或 IExtenderProviderService
。您甚至真的不知道这些具体类是什么。 .NET 实际上有自己的 IoC 容器 IContainer
,用于此目的,而 Component
类则有一个 GetService
方法,它是实际的服务定位器。当然,您可以在没有 IContainer
或该特定定位器的情况下使用任何或所有这些接口。这些服务本身与容器的耦合度很低。
WCF 中的协议完全基于接口构建。实际的具体服务类通常在配置文件中以名称引用,这本质上就是 DI。许多人并没有意识到这一点,但完全有可能使用另一个 IoC 容器来替换此配置系统。更有趣的是,服务行为都是 IServiceBehavior
的实例,可以稍后添加。同样,您可以轻松地将其连接到 IoC 容器并让其选择相关的行为,但是即使没有 IoC 容器,这个特性也可以完全使用。
IContainer
是一个不错的选择,因为它是 .NET Framework 的一部分。IContainer
实际上不是容器,这可能需要一个段落。 ;) - Aaronaught以下是原始答案:
在开发SolrNet时,我不得不面临同样的决定。我最初的目标是友好的依赖注入和容器无关性,但随着我添加更多的内部组件,内部工厂很快就变得难以管理,结果导致库缺乏灵活性。
最终,我写了一个非常简单的嵌入式IoC容器,同时提供了一个Windsor facility和一个Ninject module。将库与其他容器集成只需要正确地连接组件,因此我可以轻松地将其与Autofac、Unity、StructureMap等集成。
这样做的缺点是,我失去了只需new
服务的能力。我还依赖于CommonServiceLocator,本来可以避免的(未来可能进行重构),以使嵌入式容器更易于实现。MassTransit 似乎依赖于类似的东西。它有一个 IObjectBuilder 接口,实际上是 CommonServiceLocator 的 IServiceLocator 带有几个更多的方法,然后为每个容器实现这个接口,即 NinjectObjectBuilder 和一个常规的模块/设施,即 MassTransitModule。然后它 依赖于 IObjectBuilder 来实例化所需的内容。当然,这是一种有效的方法,但个人不太喜欢它,因为它实际上过度传递了容器,并将其用作服务定位器。
MonoRail 实现了自己的容器,该容器实现了老旧的IServiceProvider。这个容器通过一个公开已知服务的接口在整个框架中使用。为了获取具体的容器,它有一个内置的服务提供程序定位器。Windsor facility 将此服务提供程序定位器指向 Windsor,使其成为所选的服务提供程序。
底线是:没有完美的解决方案。与任何设计决策一样,这个问题需要在灵活性、可维护性和便利性之间取得平衡。