这三个工具,应该是每个MVC程序员的兵工厂中的一部分。DI容器,单元测试框架,mocking 工具。Ninject是我们偏爱的DI容器,它简单,高雅,并且容易使用。这里有很多复杂的替代品,但是我们喜欢Ninject最小配置的工作方式。如果你不喜欢Ninject,可以使用Unity,它是微软提供的替代品。
单元测试方面,我们使用VS2010内置的 NUnit,它是.Net 单元测试框架中最受欢迎的一个。
Mocking 工具套装,我们选择 Moq。如果你不喜欢它,可以使用Rhino Mocks这个不错的替代品。
1 使用 Ninject
在原书中第四章,The MVC Pattern 中,我们介绍过DI的思想,用来在我们的MVC程序中的组件解耦。为了做到这点,我们需要结合接口和DI。
1 class Program 2 { 3 staticvoid Main(string[] args) 4 { 5 ShoppingCart cart =new ShoppingCart(new LinqValueCalculator()); 6 Console.WriteLine("Total:{0}", cart.CalculateStockValue()); 7 Console.ReadKey(); 8 } 9 } 10 11 publicclass Product 12 { 13 privatestring name; 14 publicint ProductID { get; set; } 15 publicstring Name 16 { 17 get { return name; } 18 set { name = value; } 19 } 20 publicstring Description { get; set; } 21 publicdecimal Price { get; set; } 22 publicstring Category { get; set; } 23 } 24 25 publicinterface IValueCalculator 26 { 27 decimal ValueProducts(params Product[] products); 28 } 29 30 publicclass LinqValueCalculator : IValueCalculator 31 { 32 publicdecimal ValueProducts(params Product[] products) 33 { 34 return products.Sum(prod => prod.Price); 35 } 36 } 37 38 publicclass ShoppingCart 39 { 40 private IValueCalculator calculator; 41 42 public ShoppingCart(IValueCalculator calcParam) 43 { 44 calculator = calcParam; 45 } 46 47 publicdecimal CalculateStockValue() 48 { 49 Product[] products =50 { 51 new Product() { Name ="Kayak", Price = 275M}, 52 new Product() { Name ="Lifejacket", Price =48.95M}, 53 new Product() { Name ="Soccer ball", Price =19.50M}, 54 new Product() { Name ="Stadium", Price = 79500M} 55 }; 56 decimal totalValue = calculator.ValueProducts(products); 57 return totalValue; 58 } 59 }
IValueCalculator 接口中定义了一个方法,它接受一个或多个Product对象,并返回累积的值。我们将接口部署在 LinqValueCalculator类上,它使用Linq延期方法Sum,灵巧地生成Product对象们的Price属性合计。然后,我们需要创建一个会使用到IVaueCalculator的类,即ShoppingCart,这个类是为DI设计的。Shoppingcart类的构造器需要一个实现了IValueCalculator接口的类,作为参数,为DI做准备。CalculateStockValue方法创建一个Product对象数组,然后调用IValueCalculator接口中的ValueProducts,来得到合计。
我们成功地将ShoppingCart类和LinqValueCalculator类解耦,这两个类都依赖IValueCalculator,但是ShppingCart与LinqValueCalculator没有直接关系。事实上,它甚至不知道LinqValueCalculator的存在。我们能改变LinqValueCalculator的实现,甚至用一个新的IValuecalculator的实现完全替代它。
Product类与这三个类有直接关系,我们不用担心这点。Product 是一个 domain model 类型的等价物,我们期望这样的类,强耦合,依赖我们的程序。如果我们不构建MVC程序,我们也许持不同的观点,并解耦Product。
我们的目标是能够创建一个ShoppingCart的实例,并注入一个IValueCalculator类的实现,作为构造函数的参数。这是我们偏爱的DI容器Ninject所扮演的角色。但是在我们展示Ninject之前,我们需要设置VS。
1.1 使用Ninject开始
要开始使用Ninject,我们需要创建一个Ninject kernel的实例,这个对象,我们会用来与Ninject交流。
1 IKernel ninjectKernel =new StandardKernel();
一旦创建kernel,Ninject会完成两个阶段的工作。第一是绑定你想要使用你已经创建的接口关联的类型。在这种情况下,我们想要告诉Ninject,当它收到一个请求,请求IValueCalculator的实例时,它应该创建并返回一个LinqValueCalculator类的实例。我们用定义在IKernel接口中的Bind和To方法做这样的事情。
1 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator<();
这段声明将IValueCalculator接口绑定到LinqValueCalculator实例类上。我们指定我们想要注册的接口,将它作为Bind方法的一般类型参数,并传递我们想要的具体实例的类型,作为第二个参数。
第二阶段,是使用Ninject的Get方法,创建一个实施接口的对象,并将它传递给ShoppingCart类的构造器。
1 IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); 2 ShoppingCart cart =new ShoppingCart(calcImpl); 3 Console.WriteLine("Total:{0:c}", cart.TotalPrice());
我们指定我们想要实例化的接口,作为Get方法的一般类型参数。Ninject浏览我们定义的绑定,看到我们将IValueCalculator绑定到LinqValueCalculator,然后为我们创建一个新的实例。我们然后将实例注入到ShoppingCart类的构造器,并调用TotalPrice方法,它会反过来调用接口中定义的方法。
1 ShoppingCart cart =new ShoppingCart(new LinqValueCalculator());
可以简化为这样。
1.2 创建依赖链
当我们请求Ninject创建一个类型,它会检查类型之间的耦合。如果有附加选项,Ninject解决他们,并创建所有必须的类的实例。
1 publicinterface IDiscountHelper 2 { 3 decimal ApplyDiscount(decimal totalParam); 4 } 5 6 publicclass DefaultDiscountHelper : IDiscountHelper 7 { 8 publicdecimal ApplyDiscount(decimal totalParam) 9 { 10 return (totalParam - (10m / 100m * totalParam)); 11 } 12 }
IDiscounHelper定义了一个ApplyDiscount方法,它会应用一个decima值折扣。DefaultDiscounterHelper类实现这个接口。我们可以将IDiscountHelper借口添加为LinqValueCalculator的依赖。
1 publicclass LinqValueCalculator : IValueCalculator 2 { 3 private IDiscountHelper discounter; 4 5 public LinqValueCalculator(IDiscountHelper discountParam) 6 { 7 discounter = discountParam; 8 } 9 10 publicdecimal ValueProducts(params Product[] products) 11 { 12 return discounter.ApplyDiscount(products.Sum(prod => prod.Price)); 13 } 14 }
最新添加的构造器,需要传递一个IDiscountHelper接口的实现,它被用在ValueProducts方法,在处理累积Product对象的值时,应用打折。我们使用Ninject kernel将IDiscountHelper接口绑定到类的实现上。
1 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 2 ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>(); 3 IValueCalculator calImpl = ninjectKernel.Get<IValueCalculator>(); 4 ShoppingCart cart =new ShoppingCart(calImpl);
我们不用不用改变任何代码来创建IValueCalculator的实现。当需要IValueCalculator时,Ninject知道我们想要LinqValueCalculator类被实例化。他已经检验过这个类,并发现它基于一个接口实现。Ninject创建一个DefaultDiscountHelper的实例,将它注入到LinqValueCalculator类的构造器,并将结果作为IValueCalculator返回。Ninject检查所有用这种方式实例化依赖的类,无论它的依赖链有多长或多复杂。
1.3 指定属性和参数的值
我们能配置Ninject创建的类,来提供当我们将接口绑定到它的实现上时的属性细节。我们修正了StandardDiscountHelper类,使它暴漏一个方便的属性,来指定折扣的程度。
1 publicclass DefaultDiscountHelper : IDiscountHelper 2 { 3 publicdecimal DiscountSize { get; set; } 4 5 publicdecimal ApplyDiscount(decimal totalParam) 6 { 7 return (totalParam - (DiscountSize / 100m * totalParam)); 8 } 9 }
当我们使用Ninject将具体的类绑定到类型,我们可以使用WithPropertyValue方法,设置DefaultDiscountHelper类中DiscountSize属性的值。
1 ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize",50M);
我们必须以字符串的形式提供属性的名字。我们不用改变任何其他的绑定,也不用改变Get方法的使用方式。属性的值,会随着DefaultDiscountHelper构建时设置。
如果你有多个值需要设置,可以链式调用WithPropertyValue方法。
也可以给构造函数传递参数
1 publicdecimal discountRate; 2 3 public DefaultDiscountHelper(decimal discountParam) 4 { 5 discountRate = discountParam; 6 } 7 8 ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
1.4 使用自绑定
自绑定,是将Ninject完全整合进你代码的一个有用的特性,具体的类能从Ninject kernel请求。这看起来像是在做一件无聊的事情,但意味着我们不需要像以下代码那样,手工执行初始化DI。
1 IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); 2 ShoppingCart cart =new ShoppingCart(calcImpl);
而是可以简单地请求一个ShoppingCart实例,让Ninject挑选出依赖于IValueCalculator类。
1 ShoppingCart cart = ninjectKernel.Get<ShoppingCart>();
如果我们花时间注册一个自绑定类型,我们能在接口上使用这些特性,像为构造器参数和属性指定值。要注册自绑定,偶们使用ToSelf方法
1 ninjectKernel.Bind<ShoppingCart>().ToSelf().WithParameter("<parameterName>", <paramvalue>);
ShoppingCart绑定自身,调用WithParameter方法,为虚构的属性提供值。你可以仅在具体的类上使用自绑定。
1.5 绑定到派生类型
尽管我们关注接口,我们也使用Ninject绑定具体的类。我们既能绑定具体的类自己,也能绑定到一个派生类型。
1 publicclass ShoppingCart 2 { 3 protected IValueCalculator calculator; 4 protected Product[] products; 5 6 public ShoppingCart(IValueCalculator calcParam) 7 { 8 calculator = calcParam; 9 products =new[] 10 { 11 new Product{Name="Jiangyou",Price=5M}, 12 new Product{Name="Zhijin",Price=2.5M} 13 }; 14 } 15 16 publicvirtualdecimal CalculateStockValue() 17 { 18 decimal total = calculator.ValueProducts(products); 19 return total; 20 } 21 } 22 23 publicclass LimitShoppingCart : ShoppingCart 24 { 25 public LimitShoppingCart(IValueCalculator calcParam): base(calcParam) 26 { 27 // 28 } 29 30 publicoverridedecimal CalculateStockValue() 31 { 32 var filteredProducts = products.Where(e => e.Price < ItemLimit); 33 return calculator.ValueProducts(filteredProducts.ToArray()); 34 } 35 publicdecimal ItemLimit { get; set; } 36 } 37 staticvoid Main(string[] args) 38 { 39 IKernel ninjectKernel =new StandardKernel(); 40 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 41 ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M); 42 ninjectKernel.Bind<ShoppingCart>().To<LimitShoppingCart>().WithPropertyValue("ItemLimit", 3M); 43 ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); 44 Console.WriteLine(cart.CalculateStockValue()); 45 Console.ReadKey(); 46 }
我们能绑定父类,这样当我们从Ninject请求一个它的实例时,派生类的一个实例会被创建。这个技术用来绑定抽象类到它的具体实现时,工作的非常好。
1.6 使用条件绑定
使用Ninject,我们能绑定同一个接口的多个实现,或者同一个类的多个派生,使用指令,告诉它在不同的情况下应该使用哪个。下面我们创建一个IValueCalculator接口的新的实现:
1 publicclass IterativeValueCalculator : IValueCalculator 2 { 3 publicdecimal ValueProducts(params Product[] products) 4 { 5 decimal total =0; 6 foreach (Product p in products) 7 { 8 total += p.Price; 9 } 10 return total; 11 } 12 } 13 14 ninjectKernel.Bind<IValueCalculator>().To<IterativeValueCalculator>().WhenInjectedInto<LimitShoppingCart>();
我们对IValueCalculator有一个原始的绑定,Ninject试图找到最匹配的绑定,如果条件不满足,它会使用默认绑定,到相同的类和接口。所以Ninject有一个退回的值。最有用的条件绑定方法:
Method |
Effect |
When(predicate) |
当条件中的Lambda表达式等于true时 |
WhenClassHas<T>() |
当类注入含有一个指定类型的属性 |
WhenInjectedInto<T>() |
当类被注入到类型T |
1.7 在mvc中使用Ninject
首先要创建一个派生自System.Web.Mvc.DefaultControllerFactory的类。DefaultControllerFactory类是MVc用来默认创建controller 类的使用。
这类创建了一个Ninject kernel,并使用它来为通过GetControllerInstance方法创建的,当它想要的一个controller对象时,被MVC框架调用的controller类,的请求服务。我们不需要使用Ninject明确地绑定controller类。我们依靠默认的自绑定特性,自从controller成为System.Web.Mvc.Controller派生的一个具体类。
AddBindings方法,允许我们为想要保持低耦合的套件和其他组件添加其他的Ninject绑定。我们也能使用这个方法,做诶一个绑定controller类的时机——需要附加构造器参数或属性值。
一旦我们创建了这个类,我们必须使用MVC框架注册它。
现在,MVC框架会使用我们的NinjectControllerFactory来获得controller类的实例,Ninject会自动处理DI到controller。