MVC开发之注入容器Ninject的使用

背景


在不使用注入容器之前,我们的项目往往存在着大量耦合的类,这使得我们在开发大型项目时难以维护。比如下面这个简单的MVC框架的例子,计算购物车的产品价格总和:
  1. /// <summary>
  2. /// 产品模型类
  3. /// </summary>
  4. public class Product
  5. {
  6. public int ProductID { get; set; }
  7. public string Name { get; set; }
  8. public string Description { get; set; }
  9. public decimal Price { get; set; }
  10. public string Category { get; set; }
  11. }
  12. /// <summary>
  13. /// 计算器类:使用LINQ的sum方法来计算产品的总价
  14. /// </summary>
  15. public class LinqValueCalculator
  16. {
  17. public decimal ValueProducts(IEnumerable<Product> products) {
  18. return products.Sum(x=>x.Price);
  19. }
  20. }
  21. /// <summary>
  22. /// 购物车类:表示Product对象的集合,并使用计算器类LinqValueCalculator来计算总价【Product集合作自动属性,利用构造器初始化计算器类,再定义一个方法,里面调用计算器类的方法】
  23. /// </summary>
  24. public class ShoppingCart
  25. {
  26. private LinqValueCalculator calc;
  27. public IEnumerable<Product> Products { get; set; }
  28. public ShoppingCart(LinqValueCalculator calc) {
  29. this.calc = calc;
  30. }
  31. public decimal CalculateProductTotal() {
  32. return calc.ValueProducts(Products);
  33. }
  34. }
  35. /// <summary>
  36. /// 控制器:要先实例化计算器类,再把计算器类作参数带入购物车类的构造器中,然后调用购物车类的方法
  37. /// </summary>
  38. public class HomeController : Controller
  39. {
  40. private Product[] products={
  41. new Product{Name="Kayak",Category="Watersports",Price=275m},
  42. new Product{Name="Lifejacket",Category="Watersports",Price=48.95m},
  43. new Product{Name="Soccer ball",Category="Soccer",Price=19.50m},
  44. new Product{Name="Corner flag",Category="Soccer",Price=34.95m}
  45. };
  46. public ActionResult Index()
  47. {
  48. LinqValueCalculator calc = new LinqValueCalculator();
  49. ShoppingCart shoppingCart = new ShoppingCart(calc) {
  50. Products=products
  51. };
  52. decimal result = shoppingCart.CalculateProductTotal();
  53. return View(result);
  54. }
  55. }
最后view视图呈现的结果:
 在该项目中依赖一些紧耦合的类:ShoppingCart类与LinqValueCalculator类是紧耦合的,而HomeController类与ShoppingCart和LinqValueCalculator都是紧耦合的。这意味着,如果想替换LinqValueCalculator类,就必须在与它有紧耦合关系的类中找到对它的引用,并修改。这对于一个实际项目中,就是一个乏味且易错的过程。

运用接口


  1. /// <summary>
  2. /// 接口:从计算器的实现中抽象出其功能定义
  3. /// </summary>
  4. public interface IValueCalculator
  5. {
  6. decimal ValueProducts(IEnumerable<Product> products);
  7. }
  8. /// <summary>
  9. /// 计算器类:继承接口
  10. /// </summary>
  11. public class LinqValueCalculator:IValueCalculator
  12. {
  13. public decimal ValueProducts(IEnumerable<Product> products) {
  14. return products.Sum(x=>x.Price);
  15. }
  16. }
  17. /// <summary>
  18. /// 购物车类
  19. /// </summary>
  20. public class ShoppingCart
  21. {
  22. //都改为传递接口,解除ShoppingCart与ValueCalculator之间的耦合
  23. private IValueCalculator calc;
  24. public IEnumerable<Product> Products { get; set; }
  25. public ShoppingCart(IValueCalculator calc)
  26. {
  27. this.calc = calc;
  28. }
  29. public decimal CalculateProductTotal() {
  30. return calc.ValueProducts(Products);
  31. }
  32. }
  33. /// <summary>
  34. /// 控制器:要先实例化计算器类,再把计算器类作参数带入购物车类的构造器中,然后调用购物车类的方法
  35. /// </summary>
  36. public class HomeController : Controller
  37. {
  38. private Product[] products={
  39. new Product{Name="Kayak",Category="Watersports",Price=275m},
  40. new Product{Name="Lifejacket",Category="Watersports",Price=48.95m},
  41. new Product{Name="Soccer ball",Category="Soccer",Price=19.50m},
  42. new Product{Name="Corner flag",Category="Soccer",Price=34.95m}
  43. };
  44. public ActionResult Index()
  45. {
  46. //但是Home控制器和LinqValueCalculator类还是紧耦合关系
  47. IValueCalculator calc = new LinqValueCalculator();
  48. ShoppingCart shoppingCart = new ShoppingCart(calc) {
  49. Products=products
  50. };
  51. decimal result = shoppingCart.CalculateProductTotal();
  52. return View(result);
  53. }
  54. }

Ninject

Ninject的出现就是要解决Home控制器和计算器类LinqValueCalculator之间耦合的问题

1.将Ninject添加到Visual Studio项目中

选择“工具”-》“库包管理器”-》“管理解决方案的NuGet包”-》打开“NuGet包”对话框,点击左侧面板的“在线”,在右上角的搜索框中输入“Ninject”,将会找到一系列的Ninject包。本人实际开发中,遇到搜索不到的情况,这时点击左下角的设置按钮,弹出设置的对话框,把图中的两个数据源都勾选上,再回来搜索就可以找到了。
 

 2.创建依赖解析器

示例要做的第一个修改就是创建一个自定义的依赖解析器。MVC框架需要使用依赖解析器来创建类的实例,以便对请求进行服务。
给项目添加一个名为InfraStructure的新文件夹,并添加一个名为NinjectDependencyResolver.cs的类。
该类分三步走
1)创建一个Ninject内核的实例StandardKernel,用它与Ninject进行通信;
2)应用实例的方法,建立程序中接口与实现类的绑定,Bind<.Type1>().To<Type2>();
3)实际使用Ninject,应用Get方法,告诉Get方法用户感兴趣的是哪个接口,即可创建该接口绑定的实例。
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Web.Mvc;
  4. using EssentialTools2.Models;
  5. using Ninject;
  6. namespace EssentialTools2.Infrastructure
  7. {
  8. /// <summary>
  9. /// 依赖注入器类:MVC框架在需要一个类实例以便对一个传入的请求进行服务时,会调用GetService或GetServices方法
  10. /// 依赖解析器要做的工作便是创建这个实例:这一项需要调用Ninject的TryGet和GetAll方法来完成
  11. /// </summary>
  12. public class NinjectDependencyResolver:IDependencyResolver
  13. {
  14. private IKernel kernel;
  15. public NinjectDependencyResolver() {
  16. kernel = new StandardKernel();
  17. AddBindings();
  18. }
  19. public object GetService(Type serviceType) {
  20. //TryGet方法所使用的类型参数告诉Ninject,用户感兴趣的是哪个接口,当没有合适的绑定时,会返回null,而不是抛出一个异常
  21. return kernel.TryGet(serviceType);
  22. }
  23. public IEnumerable<object> GetServices(Type serviceType) {
  24. //GetAll方法支持对单一类型的多个绑定,当有多个不同的服务提供器时,可用它
  25. return kernel.GetAll(serviceType);
  26. }
  27. private void AddBindings() {
  28. kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
  29. }
  30. }
  31. }

3.注册依赖解析器

必须告诉MVC框架,用户希望使用自己的依赖解析器,通过修改Global.asax.cs文件来完成。
  1. using System.Web.Http;
  2. using System.Web.Mvc;
  3. using System.Web.Routing;
  4. using EssentialTools2.Infrastructure;
  5. namespace EssentialTools2
  6. {
  7. public class MvcApplication : System.Web.HttpApplication
  8. {
  9. protected void Application_Start()
  10. {
  11. AreaRegistration.RegisterAllAreas();
  12. //使用自己的依赖解析器
  13. DependencyResolver.SetResolver(new NinjectDependencyResolver());
  14. WebApiConfig.Register(GlobalConfiguration.Configuration);
  15. FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
  16. RouteConfig.RegisterRoutes(RouteTable.Routes);
  17. }
  18. }
  19. }
通过这里的添加语句,能让Ninject来创建MVC框架所需的任何对象实例,这便将DI放到了这一应用程序的内核中。

4.重构Home控制器

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using System.Web.Mvc;
  6. using EssentialTools2.Models;
  7. namespace EssentialTools2.Controllers
  8. {
  9. /// <summary>
  10. /// 控制器:要先实例化计算器类,再把计算器类作参数带入购物车类的构造器中,然后调用购物车类的方法
  11. /// </summary>
  12. public class HomeController : Controller
  13. {
  14. private Product[] products={
  15. new Product{Name="Kayak",Category="Watersports",Price=275m},
  16. new Product{Name="Lifejacket",Category="Watersports",Price=48.95m},
  17. new Product{Name="Soccer ball",Category="Soccer",Price=19.50m},
  18. new Product{Name="Corner flag",Category="Soccer",Price=34.95m}
  19. };
  20. private IValueCalculator calc;
  21. //添加一个构造器,接受IValueCalculator接口的实现
  22. public HomeController(IValueCalculator calcParam) {
  23. calc=calcParam;
  24. }
  25. public ActionResult Index()
  26. {
  27. ShoppingCart shoppingCart = new ShoppingCart(calc) {
  28. Products=products
  29. };
  30. decimal result = shoppingCart.CalculateProductTotal();
  31. return View(result);
  32. }
  33. }
  34. }

创建依赖性链

当要求Ninject创建一个类型时,它会检查该类与其他类之间的耦合。如果有额外依赖,Ninject会自动解析这些依赖,并创建所有类的实例。
在Models文件夹中添加一个Discount.cs的文件,如下:
  1. namespace EssentialTools2.Models
  2. {
  3. public interface IDiscountHelper
  4. {
  5. decimal ApplyDiscount(decimal totalParam);
  6. }
  7. public class DefaultDiscountHelper : IDiscountHelper {
  8. public decimal ApplyDiscount(decimal totalParam) {
  9. return (totalParam-(10m/100m*totalParam));
  10. }
  11. }
  12. }

DefaultDiscountHelper类实现了接口,并运用了固定的10%的折扣,此时修改LinqValueCalculator类,使其计算时使用该接口
  1. /// <summary>
  2. /// 计算器类:继承接口
  3. /// </summary>
  4. public class LinqValueCalculator:IValueCalculator
  5. {
  6. private IDiscountHelper discounter;
  7. public LinqValueCalculator(IDiscountHelper discountParam) {
  8. discounter = discountParam;
  9. }
  10. public decimal ValueProducts(IEnumerable<Product> products) {
  11. return discounter.ApplyDiscount(products.Sum(x=>x.Price));
  12. }
  13. }

再在自定义的依赖解析器里增加绑定
  1. private void AddBindings() {
  2. kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
  3. kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>();
  4. }

指定属性与构造器参数值

在把接口绑定到它的实现时,可以提供想要运用到属性上的一些属性细节,以便对Ninject创建的类进行配置。修改DefaultDiscountHelper类,以使它定义一个DiscountSize属性,该属性用于计算折扣量。
  1. public class DefaultDiscountHelper : IDiscountHelper {
  2. public decimal DiscountSize { get; set; }
  3. public decimal ApplyDiscount(decimal totalParam) {
  4. return (totalParam-(DiscountSize/100m*totalParam));
  5. }
  6. }

修改Ninject的绑定方法
  1. private void AddBindings() {
  2. kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
  3. kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize",50m);
  4. }

结果:
还可以使用构造器来注入
  1. public class DefaultDiscountHelper : IDiscountHelper {
  2. //public decimal DiscountSize { get; set; }
  3. private decimal discountSize;
  4. public DefaultDiscountHelper(decimal discountParam) {
  5. discountSize = discountParam;
  6. }
  7. public decimal ApplyDiscount(decimal totalParam) {
  8. return (totalParam-(discountSize/100m*totalParam));
  9. }
  10. }

只不过Ninject的绑定也要做想要的修改,**注意:这里传的是构造器的参数**
  1. private void AddBindings() {
  2. kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
  3. //kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize",50m);
  4. kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam ", 50m);
  5. }

使用条件绑定

如果一个接口有多个不同的实现类,比如再多创建一个可以实现不同折扣的类
  1. /// <summary>
  2. /// 灵活折扣类
  3. /// </summary>
  4. public class FlexibleDiscountHelper:IDiscountHelper
  5. {
  6. public decimal ApplyDiscount(decimal totalParam) {
  7. decimal discount = totalParam > 100 ? 70 : 25;
  8. return (totalParam-(discount/100m*totalParam));
  9. }
  10. }

这时就要修改Ninject的绑定方法,告诉Ninject何时使用哪个类来实例化接口。
  1. private void AddBindings() {
  2. kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
  3. //kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize",50m);
  4. kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50m);
  5. kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>().WhenInjectedInto<LinqValueCalculator>();
  6. }

本例在适当的位置留下对IDiscountHelper的原有绑定,Ninject会尝试找出最佳匹配,而且这有助于对同一个类或接口采用一个默认绑定,以便在条件判断不能得到满足时,让Ninject能够进行回滚。

参考资料

《精通ASP.NET MVC4》

项目代码下载


附件列表

    原文地址:https://www.cnblogs.com/iwsx/p/Ninject_Learn.html