本次將考察三類工具,它們是每一位 MVC 程式員工具庫的成員:DI容器、單元測試框架和模仿工具。 1.創建一個示例項目 創建一個空 ASP.NET MVC 4 項目 EssentiaTools 。 1.1 創建模型類 在 Models 文件夾下新建 Product.cs 類文件 using Syst
本次將考察三類工具,它們是每一位 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 有許多不同的條件綁定方法,最有用的一些條件綁定如下:
源碼地址:https://github.com/YeXiaoChao/EssentiaTools