Ninject与代理模式同时使用的小技巧

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的编写效率和可读性都有很大的帮助,在这里推荐。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值