Ninject是一个在.NET平台下的非常流行的轻量级开源DI框架。和在Java平台下的Guice类似,Ninject的DI配置是在代码中实现的,而不是Spring使用的XML。使用代码进行DI配置,可以通过Compiler的类型检查避免很多配置错误,提高工作效率。同时,相比较Guice在Java平台下的位置,Ninject在.NET平台下,被使用的机会会很多。这么说是因为,在Java平台下,Spring的统治地位已经相当长了,已经基于Spring的Dependency Injection基础上,形成了Spring自己的生态系统,虽然Guice出于名门(Google),也很难撼动Spring的位置。举一个简单的例子,当我们使用Spring MVC进行Servlet开发的时候,是很难更换到Guice上的,因为整个MVC框架都是建立在Spring DI的架构上的。相反,在.NET平台下,由于微软自己的Unity实在不是很好用,而新贵MEF又在.NET4.0中只是个阉割版,要到4.5才可以作为一个完整的DI,没有了Spring或微软自己产品的一家独大,这就使很多开源的DI框架都可以有很好的生命力了。
比较各个DI框架,个人很推崇Ninject,因为它很轻量,用起来很顺手。在Ninject中,最核心的一个对象就是Kernel,我们对Kernel进行配置,然后从Kernel中获得我们需要的对象。在这篇文章里我们为了举例定义这样一个接口:
public interface IDataProvider
{
IEnumerable<Int32> GetData();
}
有了这个接口:
var kernel = new StandardKernel();
kernel.Bind<IDataProvider>()
.To<SequenceDataProvider>()
.WithConstructorArgument("start", 1)
.WithConstructorArgument("step", 1)
.WithConstructorArgument("end", 5);
var dataProvider = kernel.Get<IDataProvider>();
在上面的代码中,我们首先新建一个Kernel,然后添加了对IDataProvider这个Interface的配置,通过这个配置,我们每次调用
kernel.Get<IDataProvider>()
的时候,都会获得一个新的SequenceDataProvider对象。Ninject通过Kernel配置可以知道每一个Interface对应实现的映射。
这种映射机制可以适应大多数应用场景,但是当我们使用代理设计模式的时候就出现了问题。这是因为在代理模式中,代理和被代理的对象都会实现一个接口。假设我们有以下的一个代理类:
class DataProviderProxy : IDataProvider
{
public IDataProvider DataProvider { get; private set; }
public DataProviderProxy(IDataProvider dataProvider)
{
DataProvider = dataProvider;
}
public IEnumerable<int> GetData()
{
return DataProvider.GetData().Select(i => i + 100);
}
}
我们执行以下的代码就会发生错误:
var kernel = new StandardKernel();
kernel.Bind<IDataProvider>()
.To<SequenceDataProvider>()
.WithConstructorArgument("start", 1)
.WithConstructorArgument("step", 1)
.WithConstructorArgument("end", 5);
kernel.Bind<IDataProvider>()
.To<DataProviderProxy>();
var dataProvider = kernel.Get<IDataProvider>();
我们希望的效果是Ninject把SequenceDataProvider添加到DataProviderProxy的DataProvider中,而dataProvider变量则指向代理对象。但是Ninject通过上面的映射则会把IDataProvider映射到DataProviderProxy和SequenceDataProvider两个实现中,然后当我们要获得一个新的IDataProvider的对象的时候,Ninject由于有两个实现,就会报错。
要解决上面的问题,我们就要用到Ninject的Binding Constraint的功能了。在代理模式中,代理是会实现被代理的对象所实现的接口的,利用这个特性,我们可以使用下面的代码:
var kernel = new StandardKernel();
kernel.Bind<IDataProvider>()
.To<SequenceDataProvider>()
.WhenInjectedInto<IDataProvider>()
.WithConstructorArgument("start", 1)
.WithConstructorArgument("step", 1)
.WithConstructorArgument("end", 5);
kernel.Bind<IDataProvider>()
.To<DataProviderProxy>();
var dataProvider = kernel.Get<IDataProvider>();
比较上面的代码,我们添加了一个Constraint: .WhenInjectedInto<IDataProvider>(),这就告诉了Ninject,我们的SequenceDataProvider只可以被inject到一个实现IDataProvider的对象中,而这个对象也就是我们定义的代理,这样,我们就通过Ninject实现了对代理模式的配置。
和上面一对一的代理模式很相近,另一个常用的业务场景是我们通过代理把多个对象进行封装,比如:
public class CombinedDataProvider : IDataProvider
{
public IList<IDataProvider> DataProviders { get; private set; }
public CombinedDataProvider(IList<IDataProvider> dataProviders)
{
DataProviders = dataProviders;
}
public IEnumerable<int> GetData()
{
return DataProviders.SelectMany(provider => provider.GetData());
}
}
在CombinedDataProvider中,我们用DataProviders属性存储多个子DataProvider,然后我们在GetData方法中对子DataProvider的数据整合在一起。
在这个业务场景中我们上面介绍的Ninject配置方法就无能为力了,这是因为当我们做一下配置时:
kernel.Bind<IDataProvider>()
.To<CombinedDataProvider>();
kernel.Bind<IDataProvider>()
.To<SequenceDataProvider>()
.WhenInjectedInto<IDataProvider>()
.WithConstructorArgument("start", 1)
.WithConstructorArgument("step", 1)
.WithConstructorArgument("end", 5);
kernel.Bind<IDataProvider>()
.To<RandomDataProvider>()
.WhenInjectedInto<IDataProvider>();
虽然我们通过WhenInjectedInto解决了SequenceDataProvider和RandomDataProvider两个子类的问题,但是我们不能阻止Ninject把CombinedDataProvider自己Inject到自己的DataProviders属性中,这就会产生一个Cycle dependency的错误。那么如何解决这个问题呢?
var kernel = new StandardKernel();
kernel.Bind<IDataProvider>()
.To<CombinedDataProvider>()
.When((request) =>
{
if (request.Target != null)
{
if (typeof (IList<IDataProvider>).IsAssignableFrom(request.Target.Type))
{
return false;
}
}
return true;
});
在上面的代码中,当inject的对象是IList<IDataProvider>的时候,我们限制inject我们的代理类CombinedDataProvider,通过这种方式我们就把问题解决了。
最后,本文用到的代码都可以在Github上找到:https://github.com/mcai4gl2/ninject-proxy-pattern,在NUnit测试中,有对上面几种Ninject使用方式的总结。P.S. 测试中的Assertion使用了Fluent Assertions这个开源包,使用它对Assertion的编写效率和可读性都有很大的帮助,在这里推荐。