本次将考察三类工具,它们是每一位 MVC 程序员工具库的成员:DI容器、单元测试框架和模仿工具。
1.创建一个示例项目
创建一个空 ASP.NET MVC 4 项目 EssentiaTools 。
1.1 创建模型类
在 Models 文件夹下新建 Product.cs 类文件
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace EssentiaTools.Models { public class Product { public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Catogory { get; set; } } }
另外,再新建 LinqValueCalculator.cs 类文件,它将计算 Product 对象集合的总价。
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace EssentiaTools.Models { public class LinqValueCalculator : IValueCalculator { public decimal ValueProducts(IEnumerable<Product> products) { return products.Sum(p=>p.Price); } } }
接着,再新建一个模型类 ShoppingCart ,它表示了 Product 对象的集合,并且使用 LinqValueCalculator 来确定总价。
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace EssentiaTools.Models { public class ShoppingCart { private IValueCalculator calc; public ShoppingCart(IValueCalculator calcParam) { calc = calcParam; } public IEnumerable<Product> Products { get; set; } public decimal CalcularProductTotal() { return calc.ValueProducts(Products); } } }
1.2 添加控制器
在 Controller 文件夹下新建控制器文件 HomeController.cs
using EssentiaTools.Models; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace EssentiaTools.Controllers { public class HomeController : Controller { private Product[] products = { new Product{Name="Kayak",Catogory="Watersports",Price=275M}, new Product{Name="Lifejacket",Catogory="Watersports",Price=48.95M}, new Product{Name="Soccer ball",Catogory="Soccer",Price=19.50M}, new Product{Name="Corner flag",Catogory="Soccer",Price=34.95M} }; public ActionResult Index() { LinqValueCalculator calc = new LinqValueCalculator(); ShoppingCart cart = new ShoppingCart(calc) { Products = products }; decimal totalValue = cart.CalcularProductTotal(); return View(totalValue); } } }
1.3 添加视图
根据动作方法,创建对应的视图文件 Index.cshtml
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Value</title> </head> <body> <div> Total value is $@Model </div> </body> </html>
运行程序,效果如下:
2.使用 Ninject
Ninject 是人们所喜欢的DI容器,它简单、优雅且易用。
DI容器,其思想是对MVC 应用程序中的组件进行解耦,这是通过接口与DI 相结合来实现的。
2.1 理解问题
在前面示例中,构造了一个能够用 DI 解决的问题。该项目依赖于一些紧耦合的类:ShoppingCart 类与 LinqValueCalculator 类是紧耦合的,而 HomeController 类与 ShoppingCart 和 LinqValueCalculator 都是紧耦合的。这意味着,如果想替换 LinqValueCalculator 类,就必须在与它有紧耦合关系的类中找出对它的引用,并进行修改。对这种简单的项目来说,这不是问题。但是,在一个实际项目中,这可能会成为一个乏味且易错的过程,尤其是,如果想在两个不同的计算器实现之间进行切换,而不只是用另一个类来替换另一个类的情况下。
运用接口
通过使用 C# 接口,从计算器的实现中抽象出其功能定义,可以解决部分问题。在 Models 文件夹下添加接口文件 IValueCalculator:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EssentiaTools.Models { public interface IValueCalculator { decimal ValueProducts(IEnumerable<Product> products); } }
然后,可以在 LinqValueCalcular 类中实现这一接口:
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace EssentiaTools.Models { public class LinqValueCalculator: IValueCalculator { public decimal ValueProducts(IEnumerable<Product> products) { return products.Sum(p=>p.Price); } } }
该接口能够打断 ShoppingCart 与 LinqValueCalcular 类之间的紧耦合关系,将该接口运用于 ShoppingCart 类:
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace EssentiaTools.Models { public class ShoppingCart { private IValueCalculator calc; public ShoppingCart(IValueCalculator calcParam) { calc = calcParam; } public IEnumerable<Product> Products { get; set; } public decimal CalcularProductTotal() { return calc.ValueProducts(Products); } } }
在上述过程中,已经解除了 ShoppingCart 与 LinqValueCalculator 之间的耦合,因为在使用 ShoppingCart时,只要为其构造器传递一个 IValueCalculator 接口对象就行了。于是,SHoppingCart 类与 IValueCalculator 的实现类之间不再有直接联系,但是C# 要求在接口实例化时需要指定其实现类。这很自然,因为它需要知道用户想用的是哪一个实现类。这意味着,Home 控制器在创建 LinqValueCalculatoe 对象时仍有问题。
... public ActionResult Index(){ IValueCalculator calc = new LinqValueCalculator(); ShoppingCart cart = new ShoppingCart(calc) { Products = products }; decimal totalValue = cart.CalcularProductTotal(); return View(totalValue); } ...
使用 Ninject 的目的就是要解决这一问题,用以对 IValueCalculator 接口的实现进行实例化,但所需的实现细节却又不是 Home 控制器代码的一部分(即,通过 Ninject,可以去掉 Home 控制器中的这行黑体语句所示的代码,这项工作由 Ninject 完成,这样便去掉了 Home 控制器与总价计算器 LinqValueCalculator 之间的耦合)
2.2 将 Ninject 添加到 Visual Studio 项目
通过工具菜单找到 “管理解决方案的 NuGet 程序包”选项,添加 Ninject:
或者直接右击项目名,找到 “管理 NuGet 程序包” 选项也可以添加 Ninject
2.3 Ninject 初步
为了得到 Ninject 的基本功能,要做的工作有三个步骤。给 Index 动作方法添加基本的 Ninject 功能代码如下:
using EssentiaTools.Models; using Ninject; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace EssentiaTools.Controllers { public class HomeController : Controller { private Product[] products = { new Product{Name="Kayak",Catogory="Watersports",Price=275M}, new Product{Name="Lifejacket",Catogory="Watersports",Price=48.95M}, new Product{Name="Soccer ball",Catogory="Soccer",Price=19.50M}, new Product{Name="Corner flag",Catogory="Soccer",Price=34.95M} }; public ActionResult Index() { //第一步,准备使用 Ninject,创建一个 Ninject 内核的实例。 //这是一个对象,它与Ninject 进行通信,并请求接口的实现。 IKernel ninjectKernel = new StandardKernel(); //第二步,建立应用程序中的接口和想要使用的实现类之间的关系 //Ninject 使用C# 的类型参数创建了一种关系: //将想要使用的接口为 Bind 方法的类型参数,并在其返回的结果上调用To 方法。 //将希望实例化的实现类设置为 To 方法的类型参数。 //该语句告诉 Ninject: //当要求它实现 IValueCalculator 接口时,应当创建 LinqValueCalculator 类的新实例,以便对请求进行服务。 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); //第三步,实际使用 Ninject,通过其 Get 方法完成这一工作。 //Get 方法所使用的类型参数告诉 Ninject ,用户感兴趣的是哪一个接口, //而该方法的结果是刚才用 To 方法指定的实现类型的一个实例。 IValueCalculator calc = ninjectKernel.Get<IValueCalculator>();
ShoppingCart cart = new ShoppingCart(calc) { Products = products }; decimal totalValue = cart.CalcularProductTotal(); return View(totalValue); } } }
2.4 建立MVC 依赖性注入
上面Index 动作方法所展示的三个步骤的结果是:在 Ninject 中已经建立了一些相关的知识,即使用哪一个实现类来完成对 IValueCalculator 接口的请求。但是,应用程序未做任何改进,因为 Home 控制器与 LinqValueCalculator 类仍然是紧耦合的。
创建依赖解析器
示例要做的第一个修改时创建一个自定义的依赖解析器。MVC 框架需要使用依赖解析器来创建类的实例,以便对请求进行服务。通过创建自定义解析器,保证每当要创建一个对象时,便使用 Ninject 。
在项目中添加新文件夹 Infrastructure ,并添加一个类文件 NinjectDependencyResolver.cs
using EssentiaTools.Models; using Ninject; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace EssentiaTools.Infrastructure { public class NinjectDependencyResolver:IDependencyResolver { private IKernel kernel; public NinjectDependencyResolver() { kernel = new StandardKernel(); AddBindings(); } public object GetService(Type serviceType) { return kernel.TryGet(serviceType); } public IEnumerable<object> GetServices(Type serviceType) { return kernel.GetAll(serviceType); } private void AddBindings() { kernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); } } }
MVC 框架在需要一个类实例以便对一个传入的请求进行服务时,会调用GetService 或 GetServices 方法。依赖解析器要做的工作便是创建这一实例 —— 这是一项要通过 Ninject 的 TryGet 和 GetAll 方法来完成的任务。 TryGet 方法的工作方式类似于前面所用的Get 方法,但当没有合适的绑定时,它会返回 null ,而不是抛出异常。GetAll 方法支持对单一类型的多个绑定,当多个不同的服务提供器可用时,可以使用它。
上述依赖解析器类也是建立 Ninject 绑定的地方。在AddBindings 方法中,本例用Bind 和 To 方法建立了 IValueCalculator 接口和 LinqValueCalculator 类之间的关系。
注册依赖解析器
必须告诉 MVC 框架,用户希望使用自己的依赖解析器,此事可以通过修改 Global.asax 文件来完成。
using EssentiaTools.Infrastructure; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Routing; namespace EssentiaTools { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); //通过这里添加的语句,能让 Ninject 来创建 MVC 框架所需的任何对象实例, //这便将 DI 放到了这一示例应用程序中 DependencyResolver.SetResolver(new NinjectDependencyResolver()); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); } } }
重构 Home 控制器
最后一个步骤是重构 Home 控制器,以使它能够利用前面所建立的工具。
using EssentiaTools.Models; using Ninject; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace EssentiaTools.Controllers { public class HomeController : Controller { private Product[] products = { new Product{Name="Kayak",Catogory="Watersports",Price=275M}, new Product{Name="Lifejacket",Catogory="Watersports",Price=48.95M}, new Product{Name="Soccer ball",Catogory="Soccer",Price=19.50M}, new Product{Name="Corner flag",Catogory="Soccer",Price=34.95M} }; private IValueCalculator calc; public HomeController(IValueCalculator calcParam) { calc = calcParam; } public ActionResult Index() { ShoppingCart cart = new ShoppingCart(calc) { Products = products }; decimal totalValue = cart.CalcularProductTotal(); return View(totalValue); } } }
所做的主要修改时添加了一个构造器,它接受 IValueCalculator 接口的实现。示例并未指定想要使用的是哪一个实现,而且已经添加了一个名为“calc”的实例变量,可以在整个控制器中用它来表示构造器所接收到的 IValueCalculator 。
所做的另一个修改时删除了任何关于 Ninject 或 LinqValueCalculator 类的代码 —— 最终打破了 HomeController 和 LinqValueCalculator 类之间的紧耦合。
运行结果和前例一样:
示例创建的是一个构造器注入示例,这是依赖性注入的一种形式。以下是运行示例应用程序,且 Internet Explorer 对应用程序的跟 URL 发送请求时所发生的情况。
(1)浏览器向 MVC 框架发送一个请求 Home 的URL, MVC 框架猜出该请求意指 Home 控制器,于是会创建 HomeController 类实例。
(2)MVC 框架在创建 HomeController 类实例过程中会发现其构造器有一个对 IValueCalculator 接口的依赖项,于是会要求依赖性解析器对此依赖项进行解析,将该接口指定为依赖性解析器中 GetService 方法所使用的类型参数。
(3)依赖项解析器会将传递过来的类型参数交给 TryGet 方法,要求 Ninject 创建一个新的 IValueCalculator 接口类实例。
(4)Ninject 会检测到该接口与其实现类 LinqValueCalculator 具有绑定关系,于是为该接口创建一个 LinqValueCalculator 类实例,并将其回递给依赖性解析器。
(5)依赖性解析器将 Ninject 所返回的 LinqValueCalculator 类作为 IValueCalculator 接口实现类实例回递给 MVC 框架。
(6)MVC 框架利用依赖性解析器返回的接口类实例创建 HomeController 控制器实例,并使用控制器实例度请求进行服务。
这里所采取的办法其好处之一是,任何控制器都可以在其构造器中声明一个 IValueCalculator ,并通过自定义依赖性解析器使用 Ninject 来创建一个在 AddBindings 方法中指定的实现实例。
所得到的最大好处是,在希望用另一个实现来替代 LinqValueCalculator 时,只需要对依赖性解析器类进行修改,因为为了满足对于 IValueCalculator 接口的请求,这里是唯一一处为该接口指定实现类的地方。
创建依赖性链
当要求 Ninject 创建一个类型时,它会检查该类型与其他类型之间的耦合。如果有额外的依赖性, Ninject 会自动地解析这些依赖性,并创建所需要的所有类型实例。
在 Models 文件夹下新建文件 Discount.cs ,并定义了一个新的接口及其实现类:
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace EssentiaTools.Models { public interface IDiscountHelper { //将一个折扣运用于一个十进制的值 decimal ApplyDiscount(decimal totalParam); } public class DefaultDiscountHelper : IDiscountHelper { //实现接口 IDiscountHelper,并运用固定的10% 折扣 public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (10m / 100m * totalParam)); } } }
然后在 LinqValueCalculator 类中添加一个依赖性,修改 LinqValueCalculator 类,以使它执行计算时使用 IDiscountHelper 接口:
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace EssentiaTools.Models { public class LinqValueCalculator : IValueCalculator { private IDiscountHelper discounter; public LinqValueCalculator(IDiscountHelper discountParam) { discounter = discountParam; } public decimal ValueProducts(IEnumerable<Product> products) { return discounter.ApplyDiscount(products.Sum(p => p.Price)); } } }
新添加的构造器以 IDiscountHelper 的接口实现为参数,然后将其用于 ValueProducts 方法,以便对所处理的 Products 对象的累计值运用一个折扣。
就像对 IValueCalculator 所做的那样,在 NinjectDependencyResolver 类中,用 Ninject 内核将 IDiscountHelper 接口与其实现类进行了绑定,将另一个接口绑定到它的实现:
using EssentiaTools.Models; using Ninject; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace EssentiaTools.Infrastructure { public class NinjectDependencyResolver:IDependencyResolver { private IKernel kernel; public NinjectDependencyResolver() { kernel = new StandardKernel(); AddBindings(); } public object GetService(Type serviceType) { return kernel.TryGet(serviceType); } public IEnumerable<object> GetServices(Type serviceType) { return kernel.GetAll(serviceType); } private void AddBindings() { kernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>(); } } }
上述这一做法已经创建了一个 Ninject 可以轻松解析的依赖性链,这是通过在自定义依赖性解析器中所定义的绑定实现的。为了满足对 HomeController 类的请求,Ninject 会意识到它需要创建一个用于 IValueCalculator 类的实现,通过考察其绑定,便会看出该接口的实现策略是使用 LinqValueCalculator 类。但在创建 LinqValueCalculator 对象过程, Ninject 又会意识到它需要使用 IDiscountHelper 接口实现,因此会查看其绑定,并创建一个 DefaultDiscountHelper 对象。 Ninject 会创建这一 DefaultDiscountHelper ,并将其传递给 LinqValueCalculator 对象的构造器, LinqValueCalculator 对象又转而被传递给 HomeController 类的构造器,最后所得到的 HomeController 用于对用户的请求进行服务。 Ninject 会以这种方式检查它要实例化的每一个依赖性类,无论其依赖性链有多长,多复杂。
2.5 指定属性与构造器参数值
在把接口绑定到它的实现时,可以提供想要运用到属性上的一些属性细节,以便对 Ninject 创建的类进行配置。修改 DefaultDiscountHelper 类,以使它定义一个 DiscountSize属性,该属性用于计算折扣量:
public class DefaultDiscountHelper : IDiscountHelper { public decimal DiscountSize { get; set; } public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (DiscountSize / 100m * totalParam)); } }
在用 Ninject 将具体类绑定到类型时,可以使用 WithPropertyValue 方法来设置 DefaultDiscountHelper 类中的 DiscountSize 属性的值。
... private void AddBindings() { kernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize",50M); } ...
注意,必须提供一个字符串值作为要设置的属性名。既不需要修改任何绑定,也不需要修改使用 Get 方法获取 ShoppingCart 类实例的方式。
该属性值会按照 DefaultDiscountHelper 的构造进行设置,并起到半价的效果。显示结果如下:
如果需要设置多个属性值,可以链接调用 WithPropertyValue 方法涵盖所有这些属性,也可以用构造器参数做同样的事情。可以重写 DefaultDiscountHelper 类如下,以便折扣大小作为构造器参数进行传递。
public class DefaultDiscountHelper : IDiscountHelper { public decimal discountSize; public DefaultDiscountHelper(decimal discountParam) { discountSize = discountParam; } public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (discountSize / 100m * totalParam)); } }
为了用 Ninject 绑定这个类,可以在 AddBindings 方法中使用 WithConstructorArgument 方法来指定构造器参数的值:
... private void AddBindings() { kernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam",50M); } ...
这一技术允许用户将一个值注入到构造器中。同样,可以将这些方法调用链接在一起,以提供多值,并与依赖性混合和匹配。 Ninject 会判断出用户的需要,并依此来创建它。
2.6 使用条件绑定
Ninject 支持多个条件的绑定方法,这能够指定用哪一个类对某一特定的请求进行响应。
在 Models 文件夹下新建类文件 FlexibleDiscountHelper.cs :
namespace EssentiaTools.Models { public class FlexibleDiscountHelper : IDiscountHelper { public decimal ApplyDiscount(decimal totalParam) { decimal discount = totalParam > 100 ? 70 : 25; return (totalParam - (discount / 100M * totalParam)); } } }
FlexibleDiscountHelper 类根据要打折的总额大小运用不同的折扣,然后修改 NinjectDependencyResolver 的 AddBindings 方法,以告诉 Ninject 何时使用 FlexibleDiscountHelper ,何时使用 DefaultDiscountHelper:
... private void AddBindings() { kernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam",50M); kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>().WhenInjectedInto<LinqValueCalculator>(); } ...
上述绑定指明,在 Ninejct 要将一个实现注入LinqValueCalculator 对象时,应该使用 FlexibleDiscountHelper 类作为 IDiscountHelper 接口的实现。
本例在适当的位置留下了对 IDiscountHelper 的原有绑定。 Ninject 会尝试找出最佳匹配,而且这有助于对同一类或接口采用一个默认绑定,以便在条件判据不能得到满足时,让 Ninject 能够进行回滚。 Ninject 有许多不同的条件绑定方法,最有用的一些条件绑定如下: