Đưa các giá trị property và constructor vào các đối tượng mới được tạo ra Tự động chọn một lớp triển khai cho một giao diện Kiểm soát vòng đời của các đối tượng mà Ninject tạo ra
Trang 1CHƯƠNG 6
Công cụ cần thiết cho MVC
Trong chương này, tôi sẽ xem xét ba công cụ mà mọi lập trình viên MVC đều cần phải có: một dependency injection (DI) container, một framework kiểm thử đơn vị, và một công cụ làm giả
Tôi đã chọn ba triển khai cụ thể của các công cụ trên cho cuốn sách này, nhưng cũng có rất nhiều lựa chọn thay thế khác cho từng loại Nếu bạn không quen thuộc với những ci mà tôi sử dụng, đừng nên lo lắng Chúng có rất nhiều, chắc chắn là bạn sẽ tìm thấy một cái gì đó phù hợp với mình
Như đã đề cập ở Chương 3, Ninject là DI container ưa thích của tôi Nó đơn giản, thanh lịch và dễ sử dụng Có nhiều lựa chọn tinh tế hơn, nhưng tôi thích cách mà Ninject làm việc mà không phải cấu hình quá nhiều Nếu bạn không thích Ninject, tôi khuyên bạn nên thử Unity, nó là một sự thay thế từ Microsoft
Đối với kiểm thử đơn vị, tôi sẽ sử dụng các công cụ kiểm thử được xây dựng sẵn trong Visual Studio Tôi từng sử dụng NUnit,
nó mà là một framework kiểm thử đơn vị NET phổ biến, nhưng Microsoft đã thực hiện một sự thúc đẩy lớn để cải thiện sự hỗ trợ kiểm thử đơn vị trong Visual Studio và giờ đây nó cũng đã được tích hợp trong các phiên bản Visual Studio miễn phí Kết quả là một framework kiểm thử đơn vị đã được tích hợp một cách chặt chẽ vào toàn bộ IDE và điều này là rất tốt
Các công cụ thứ ba tôi chọn là Moq, nó là một bộ công cụ làm giả Tôi sử dụng Moq để tạo ra các triển khai của giao diện để sử dụng trong các kiểm thử đơn vị Các lập trình viên hoặc là thích hoặc là ghét Moq; không có ai ở giữa cả Hoặc là bạn sẽ thấy thấy rằng cú pháp khá thanh lịch và biểu cảm, còn không bạn sẽ bị nguyền rủa mỗi khi bạn cố gắng sử dụng nó Nếu bạn cảm thấy không quen, tôi đề nghị bản thử Rhino Mocks, nó là một thay thế tốt
Tôi sẽ giới thiệu mỗi công cụ và trình bày các tính năng cốt lõi của chúng, nhưng tôi không đào sâu một cách đầy đủ về chúng Chi tiết về mỗi một trong số chúng đều có thể trong các sách riêng Tôi đã cho bạn đủ thông tin để bắt đầu và làm theo các ví dụ trong phần còn lại của cuốn sách Bảng 6-1 cung cấp bản tóm tắt cho chương này
Đưa các giá trị property và
constructor vào các đối tượng
mới được tạo ra
Tự động chọn một lớp
triển khai cho một
giao diện
Kiểm soát vòng đời của các đối
tượng mà Ninject tạo ra
Tạo một kiểm thử đơn vị
Kiểm tra các kết quả mong đợi
trong một kiểm thử đơn vị
Tập trung một kiểm thử đơn vị
vào một tính năng duy nhất của
Sử dụng phương thức WithPropertyValue và WithConstructorArgument
Sử dụng một ràng buộc có điều kiện của Ninject
Thiết lập một phạm vi của đối tượng Thêm một project kiểm thử đơn vị vào solution và chú thích một tập tin lớp với các thuộc tính TestClass
Trang 2Chú ý Chương này giả định rằng bạn muốn tất cả những lợi ích có được từ MVC Framework, bao gồm một kiến trúc có hỗ trợ
rất nhiều kiểm thử và nhấn mạnh vào việc tạo ra các ứng dụng có thể dễ dàng sửa đổi và bảo trì Tôi thích thứ này, nhưng tôi biết rằng một số độc giả sẽ chỉ muốn hiểu các tính năng mà các MVC Framework cung cấp và không muốn biết về phương pháp và phương châm của việc phát triển Tôi sẽ không cố gắng thay đổi bạn Đó là quyết định cá nhân và bạn biết là bạn cần làm những gì
để phát triển các dự án của mình Tôi đề nghị chí ít là bạn nên xem lướt qua qua chương này để xem những gì có sẵn, nhưng nếu bạn không phải là người thích kiểm thử đơn vị, thì bạn có thể nhảy được tới chương kế tiếp và xem làm thế nào để xây dựng một ứng dụng MVC mẫu thực tế hơn
Chuẩn bị Project Mẫu
Tôi sẽ bắt đầu bằng cách tạo ra một project mẫu đơn giản, tên là EssentialTools, mà tôi sẽ sử dụng trong suốt chương này Tôi sử dụng template ASP.NET MVC Web Application, chọn tùy chọn Empty và đánh dấu vào ô để thêm các nội dung một project MVC cơ bản
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 Category { set; get; }
public class LinqValueCalculator {
public decimal ValueProducts(IEnumerable<Product> products) { return products.Sum(p => p.Price);
}
}
}
Lớp LinqValueCalculator định nghĩa một phương thức duy nhất tên là ValueProducts, nó sử dụng
phương thức Sum của LINQ để cộng giá trị của các thuộc tính Price của từng đối tượng Product thành một số đếm được (một tính năng khá tốt của LINQ mà tôi thường sử dụng)
Lớp model cuối cùng của tôi là ShoppingCart và nó đại diện cho một bộ các đối tượng Product và sử dụng một
Trang 3LinqValueCalculator để xác định tổng giá trị Tôi đã tạo ra một tập tin lớp mới tên là ShoppingCart.cs
và thêm vào các câu lệnh như trong Liệt kê 6-3
Liệt kê 6-3 Nội dung của tập tin ShoppingCart.cs
using System.Collections.Generic;
namespace EssentialTools.Models {
public class ShoppingCart {
private LinqValueCalculator calc;
public ShoppingCart(LinqValueCalculator calcParam) {
calc = calcParam;
}
public IEnumerable<Product> Products { get; set; }
pr public decimal CalculateProductTotal() {
Tôi đã thêm một controller mới vào thư mục Controllers tên là HomeController và thiết lập nội dung khớp với
Liệt kê 6-4 Phương thức hành động của Index tạo ra một mảng các đối tượng Product và sử dụng một đối tượng LinqValueCalculator để xuất ra tổng giá trị, sau đó chuyển cho phương thức View Tôi không đặc tả một view khi tôi gọi phương thức View, vì vậy MVC Framework sẽ chọn view mặc định gắn liền với phương thức hành động (tập tin Views/Home/Index.cshtml)
public class HomeController : Controller {
private Product[] products = {
new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
new Product {Name = "Soccer ball", Category = "Soccer", Price
= 19.50M},
= 34.95M}
};
new Product {Name = "Corner flag", Category = "Soccer", Price
public ActionResult Index() {
LinqValueCalculator calc = new LinqValueCalculator();
products };
ShoppingCart cart = new ShoppingCart(calc) { Products =
Trang 4decimal totalValue = cart.CalculateProductTotal();
Bổ sung cuối cùng cho project là view , tên là Index Không quan trọng là bạn đã chọn tùy chọn gì khi tạo view, miễn là
bạn thiết lập nội dung giống như trong Liệt kê 6-5
Liệt kê 6-5 Nội dung của tập tin Index.cshtml
Tôi đã giới thiệu dependency injection (DI) ở Chương 3 Tóm lại, ý tưởng là để tách các thành phần trong một ứng dụng
MVC, với sự kết hợp của các interface và DI container nhằm tạo ra các instance của các đối tượng bằng cách tạo ra các
interface mà chúng phụ thuộc vào và đưa chúng vào trong constructor
Trang 5Trong các phần tiếp theo , Tôi sẽ giải thích một vấn đề mà tôi đã cố tình tạo ra trong ví dụ của chương này và cho thấy làm thế nào để sử dụng Ninject–DI container ưa thích của tôi–để giải quyết nó Đừng lo lắng nếu bạn cảm thấy rằng mình không quen với Ninject–các nguyên tắc cơ bản là như nhau cho tất cả các DI container và có rất nhiều lựa chọn thay thế để chọn
Hiểu được Vấn đề
Trong ứng dụng mẫu, Tôi đã tạo ra một ví dụ về một vấn đề cơ bản mà DI phải xử lý: các lớp liên kết chặt chẽ với nhau Lớp ShoppingCart liên kết chặt chẽ với lớp LinqValueCalculator và lớp HomeController liên kết chặt chẽ với cả ShoppingCart và LinqValueCalculator
Điều này có nghĩa là nếu tôi muốn thay thế lớp LinqValueCalculator, Tôi phải xác định và sau đó thay đổi các tham chiếu trong các lớp được liên kết chặt chẽ với nó Đây hẳn không phải là vấn đề gì với một project đơn giản, nhưng nó sẽ trở thành một quá trình mệt nhọc và dễ bị lỗi trong một project thực tế, đặc biệt là nếu tôi muốn chuyển đổi giữa các triển khai tính toán khác nhau (ví dụ, để kiểm thử), thay vì chỉ thay thế một lớp này với một lớp khác
Áp dụng một Interface
Tôi có thể giải quyết một phần của vấn đề bằng cách sử dụng một interface C# để trừu tượng hóa định nghĩa của các chức năng tính toán từ triển khai của nó Để chứng minh điều này, tôi đã thêm tập tin lớp IValueCalculator.cs vào thư mục Models và tạo interface như trong Liệt kê 6-6
Liệt kê 6-6 Nội dung của tập tin IValueCalculator.cs
using System.Collections.Generic;
namespace EssentialTools.Models {
public interface IValueCalculator {
decimal ValueProducts(IEnumerable<Product> products);
}
}
Tôi sau đó có thể triển khai interface này trong lớp LinqValueCalculator, như trong Liệt kê 6-7
Liệt kê 6-7 Áp dụng một Interface trong tập tin LinqValueCalculator.cs
using System.Collections.Generic;
using System.Linq;
namespace EssentialTools.Models {
public class LinqValueCalculator : IValueCalculator {
public decimal ValueProducts(IEnumerable<Product> products) { return products.Sum(p => p.Price);
Trang 6namespace EssentialTools.Models {
public class ShoppingCart {
private IValueCalculator calc;
public ShoppingCart(IValueCalculator calcParam) {
calc = calcParam;
}
public IEnumerable<Product> Products { get; set; }
public decimal CalculateProductTotal() {
return calc.ValueProducts(Products);
}
}
}
Tôi đã thực hiện một vài cải tiến, nhưng C# yêu cầu tôi đặc tả lớp triển khai cho một interface trong quá trình tạo instance, đó là
điều dễ hiểu bởi nó cần phải biết lớp triển khai nào tôi muốn sử dụng–nhưng điều đó có nghĩa là tôi vẫn còn một vấn đề trong
controller Home khi tôi tạo đối tượng LinqValueCalculator, như trong L iệt kê 6- 9
Liệt kê 6-9 Áp dụng Interface cho tập tin HomeController.cs
public ActionResult Index() {
IValueCalculator calc = new LinqValueCalculator();
ShoppingCart cart = new ShoppingCart(calc) { Products = products };
decimal totalValue = cart.CalculateProductTotal();
}
return View(totalValue);
Mục tiêu của tôi với Ninject là đạt đến điểm mà tôi xác định rằng tôi muốn khởi tạo một triển khai của interface
IValueCalculator, thế nhưng các chi tiết của việc triển khai được yêu cầu thì lại không nằm trong code của controller Home
Điều này sẽ có nghĩa là nói với Ninject rằng LinqValueCalculator là triển khai của interface IValueCalculator
mà tôi muốn nó sử dụng và cập nhật lớp HomeController để nó lấy được đối tượng của nó thông qua Ninject, thay vì sử
dụng từ khóa new
Thêm Ninject vào Visual Studio Project
Cách đơn giản nhất để thêm Ninject vào một dự án MVC là sử dụng hỗ trợ được tích hợp của Visual Studio cho NuGet, thứ giúp
việc cài đặt một loạt các gói và giữ cho chúng được cập nhật trở nên dễ dàng Tôi đã sử dụng NuGet trong Chương 2 để cài đặt
thư viện Bootstrap, nhưng có một danh mục lớn các gói có sẵn, bao gồm cả Ninject
Chọn Tools Library Package Manager Package Manager Console trong Visual Studio
để mở cửa sổ dòng lệnh NuGet và nhập vào các lệnh sau:
Install-Package Ninject -version 3.0.1.10
Install-Package Ninject.Web.Common -version 3.0.0.7
Install-Package Ninject.MVC3 -Version 3.0.0.6
Lệnh đầu tiên cài đặt các gói cốt lõi của Ninject và số còn lại thì cài đặt phần mở rộng cho lõi khiến Ninject hoạt động tốt với các ứng dụng ASP.NET (tôi sẽ giải thích ngay sau đây) Đừng hiểu nhầm với phần tham chiếu MVC3 trong tên của gói cuối cùng–
Trang 7Tôi đã sử dụng tham số version để cài đặt các phiên bản cụ thể của các gói Đây là những phiên bản mới nhất khi tôi viết cuốn sách này Bạn nên sử dụng tham số version để đảm bảo rằng bạn sẽ có được kết quả đúng trong các ví dụ của chương này, nhưng bạn có thể bỏ qua tham số này và lấy các phiên bản mới nhất (và có khả năng gần đây hơn) cho các project thực tế
Bắt đầu với Ninject
Có ba bước để làm cho các chức năng cơ bản của Ninject hoạt động, và bạn có thể xem chúng trong Liệt kê 6-10, trong
đó cho thấy những thay đổi tôi đã thực hiện với controller Home
Mẹo Tôi sẽ đi từ từ trong phần này và các phần tiếp theo Dependency Injection có thể mất một lúc mới hiểu được và
tôi không muốn bỏ qua bất cứ điều gì có thể giúp giảm bớt sự bối rối
Liệt kê 6-10 Thêm chức năng cơ bản của Ninject vào phương thức hành động của Index trong tập tin HomeController.cs
private Product[] products = {
new Product {Name = "Kayak", Category = "Watersports", Price = new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
new Product {Name = "Soccer ball", Category = "Soccer", Price
= 19.50M},
= 34.95M}
};
new Product {Name = "Corner flag", Category = "Soccer", Price
public ActionResult Index() {
ShoppingCart cart = new ShoppingCart(calc) { Products =
decimal totalValue = cart.CalculateProductTotal();
return View(totalValue);
}
}
}
Bước đầu tiên là chuẩn bị Ninject để sử dụng Để làm điều này, tôi tạo ra một instance của một Ninject kernel, đó là đối tượng
có trách nhiệm giải quyết các phụ thuộc và tạo ra các đối tượng mới Khi tôi cần một đối tượng, tôi sẽ sử dụng kernel thay vì từ khóa new Đây là câu lệnh tạo ra kernel từ bảng liệt kê:
Trang 8
IKernel ninjectKernel = new StandardKernel();
Tôi cần tạo ra một triển khai của interface Ninject.IKernel, bằng cách tạo ra một instance mới của lớp
StandardKernel Ninject có thể được mở rộng và tùy chỉnh để sử dụng các loại kernel khác nhau, nhưng tôi chỉ cần
StandardKernel được tích hợp sẵn trong chương này (Trên thực tế, tôi đã được sử dụng Ninject nhiều năm và tôi chỉ cần mỗi StandardKernel)
Bước thứ hai của quá trình này là cấu hình kernel của Ninject để nó hiểu các đối tượng triển khai mà tôi muốn sử dụng cho mỗi interface mà tôi đang làm việc với Đây là câu lệnh từ bảng liệt kê mà làm việc đó:
ninjectKernel.Bind< IValueCalculator >().To< LinqValueCalculator >();
Ninject sử dụng các tham số kiểu của C # để tạo ra một mối quan hệ: tôi thiết lập interface mà tôi muốn làm việc với như tham
số kiểu cho phương thức Bind và gọi phương thức To trên kết quả nó trả về Tôi thiết lập các lớp triển khai tôi muốn khởi tạo như tham số kiểu cho phương thức To Câu lệnh này nói với Ninject rằng các phụ thuộc trên interface IValueCalculator nên được giải quyết bằng cách tạo ra một instance của lớp LinqValueCalculator Bước cuối cùng là sử dụng Ninject để tạo ra một đối tượng, thông qua phương thức Get của kernel, như thế này:
Thiết lập MVC Dependency Injection
Kết quả của ba bước tôi đã giới thiệu trong phần trước là kiến thức về lớp triển khai cần được khởi tạo nhằm đáp ứng yêu cầu cho interface IValueCalculator vốn đã được thiết lập trong Ninject Tất nhiên, tôi vẫn chưa cải thiện được ứng dụng của mình vì kiến thức đó vẫn được xác định trong controller Home, có nghĩa là controller Home vẫn liên kết chặt chẽ với lớp LinqValueCalculator
Trong các phần sau, tôi sẽ chỉ cho bạn cách để nhúng Ninject vào trung tâm của ứng dụng MVC, điều này sẽ cho phép tôi đơn giản hóa các controller, mở rộng ảnh hưởng Ninject đã để nó hoạt động trên khắp ứng dụng, và di chuyển cấu hình ra khỏi controller
Tạo ra Dependency Resolver
Thay đổi đầu tiên tôi sẽ làm là tạo ra một custom dependency resolver MVC Framework sử dụng một dependency resolver để tạo
ra các instance của các lớp mà nó cần phải phục vụ các yêu cầu Bằng cách tạo ra một custom resolver, tôi có thể đảm bảo rằng MVC Framework sử dụng Ninject bất cứ khi nào nó tạo ra một đối tượng–bao gồm cả các instance của các controller, chẳng hạn
Để thiết lập resolver, Tôi đã tạo một thư mục mới có tên là Infrastructure, đó là thư mục mà tôi sử dụng để bỏ các lớp mà không phù hợp với các thư mục khác trong một ứng dụng MVC Tôi đã thêm một tập tin lớp mới vào thư mục có tên là NinjectDependencyResolver.cs, nội dung của nó bạn có thể xem trong Liệt kê 6-11
Liệt kê 6-11 Nội dung của tập tin NinjectDependencyResolver.cs
Trang 9namespace EssentialTools.Infrastructure {
public class NinjectDependencyResolver : IDependencyResolver {
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernelParam) {
}
}
Lớp NinjectDependencyResolver triển khai interface IDependencyResolver, đó là một phần của namespace System.Mvc và là cái mà MVC Framework sử dụng để lấy các đối tượng nó cần MVC Framework sẽ gọi các phương thức GetService hoặc GetServices khi nó cần một instance của một lớp để phục vụ yêu cầu vào Công việc của một dependency resolver là tạo ra instance đó, một nhiệm vụ mà tôi thực hiện bằng cách gọi phương thức TryGet và GetAll của Ninject Phương thức TryGet hoạt động như phương thức Get tôi đã sử dụng trước đó, nhưng nó sẽ trả về null khi không có ràng buộc phù hợp thay vì một exception Phương thức GetAll hỗ trợ nhiều ràng buộc cho một kiểu duy nhất, nó được sử dụng khi có một số các đối tượng triển khai khác nhau hiện hữu
Lớp dependency resolver của tôi cũng là nơi mà tôi thiết lập ràng buộc Ninject Trong phương thức AddBindings, tôi sử dụng phương thức Bind và To để cấu hình mối quan hệ giữa interface IValueCalculator và lớp LinqValueCalculator
Đăng ký cho Dependency Resolver
Nếu chỉ đơn thuần tạo một triển khai cho interface IDependencyResolver thôi thì vẫn chưa đủ–Tôi phải nói với MVC Framework rằng tôi muốn sử dụng nó Các gói Ninject tôi thêm vào bằng NuGet đã tạo ra một tập tin có tên là NinjectWebCommon.cs trong thư mục App_Start định nghĩa các phương thức được gọi một cách tự động khi ứng
dụng khởi chạy, để tích hợp vào vòng đời yêu cầu của ASP.NET (Điều này là để cung cấp tính năng phạm vi mà tôi sẽ mô tả sau
trong chương này) Trong phương thức RegisterServices của lớp NinjectWebCommon, tôi thêm một câu lệnh để tạo một instance của lớp NinjectDependencyResolver và sử dụng phương thức static SetResolver được định nghĩa bởi lớp System.Web.Mvc.DependencyResolver để đăng ký resolver với MVC Framework, như trong L iệ
t
kê 6- 1 2 Đừng lo lắng nếu cảm thấy điều này không hợp lý Tác dụng của câu lệnh này là để tạo ra một cầu nối giữa Ninject và
sự
hỗ trợ của MVC Framework cho DI
Liệt kê 6-12 Đăng ký Resolver trong tập tin NinjectWebCommon.cs
Trang 10Tái cấu trúc lại Controller Home
Bước cuối cùng là phải tái cấu trúc lại controller Home để nó có được lợi thế của các phương tiện mà tôi thiết lập trong các phần trước Bạn có thể thấy những thay đổi mà tôi đã thực hiện trong Liệt kê 6-13
Liệt kê 6-13 Tái cấu trúc lại Controller trong tập tin HomeController.cs
using System.Web.Mvc;
using EssentialTools.Models;
namespace EssentialTools.Controllers {
public class HomeController : Controller {
private IValueCalculator calc;
private Product[] products = {
new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
new Product {Name = "Soccer ball", Category = "Soccer", Price
= 19.50M},
= 34.95M}
};
new Product {Name = "Corner flag", Category = "Soccer", Price
public HomeController(IValueCalculator calcParam) {
calc = calcParam;
}
public ActionResult Index() {
products };
ShoppingCart cart = new ShoppingCart(calc) { Products =
decimal totalValue = cart.CalculateProductTotal();
Thay đổi khác mà tôi đã làm là bỏ bất kỳ đề cập nào về Ninject hoặc lớp LinqValueCalculator từ controller Sau cùng, tôi đã phá vỡ liên kết chặt chẽ giữa HomeController và lớp LinqValueCalculator
Nếu bạn chạy ví dụ này, bạn sẽ thấy kết quả như trong Hình 6-2 Tất nhiên, tôi có được kết quả tương tự khi tôi khởi tạo lớp LinqValueCalculator trực tiếp trong controller
Trang 11Hình 6-2 Tác dụng của việc chạy ứng dụng mẫu
Tôi đã tạo một ví dụ của constructor injection, đó là một dạng của dependency injection Dưới đây là những gì đã xảy ra khi
bạn chạy ứng dụng mẫu và Internet Explorer tạo yêu cầu cho URL gốc của ứng dụng:
1 MVC Framew ork nhận yêu cầu và tìm ra rằng yêu cầu này là dành cho controller Home (Tôi sẽ giải thích MVC framew ork làm điều này như thế nào trong Chương 17)
2 MVC Framew ork yêu cầu lớp custom dependency resolver của tôi tạo một instance của lớp HomeController, đặc
tả lớp sử dụng tham số Type của phương thức GetService
3 Dependency resolver của tôi yêu cầu Ninject tạo một lớp HomeController mới, truyền đối tượng Type cho phương thức TryGet
4 Ninjec t kiểm tr a contruct or HomeController và thấy rằng nó đã khai báo một phụ thuộc vào interface IValueCalculator, mà nó đã có một ràng buộc
5 Ninjec t tạo một ins tanc e của lớp LinqValueCalculator và sử dụng nó để tạo một ins tanc e mới của lớp HomeController
6 Ninject truyền instance HomeController cho custom dependency resolver, thứ sẽ trả nó về cho MVC Framew ork MVC Framew ork sử dụng instance của controller để phục vụ yêu cầu
Tôi làm điều này tốn chút ích công sức bởi DI có thể khá lằn nhằn khi bạn thấy nó lần đầu tiên được sử dụng Một lợi ích của
cách tiếp cận tôi đã thực hiện ở đây là bất kỳ controller nào trong ứng dụng đều có thể khai báo một phụ thuộc và MVC
Framew ork sẽ sử dụng Ninject để giải quyết nó
Quan trọng nhất là tôi chỉ phải thay đổi lớp dependency resolver khi tôi muốn thay thế LinqValueCalculator mới một triển khai khác, bởi vì đây là nơi duy nhất mà tôi phải đặc tả triển khai được sử dụng để đáp ứng các phụ thuộc vào interface IValueCalculator
Tạo Các Chuỗi Phụ Thuộc
Khi bạn yêu cầu Ninject tạo một kiểu, nó kiểm tra các phụ thuộc mà kiểu này đã khai báo Nó cũng xem xét những phụ thuộc này
để xem liệu chúng dựa vào các kiểu khác–hoặc, nói cách khác, liệu chúng có khái báo các phụ thuộc của riêng chúng Nếu có phụ
thuộc bổ sung, Ninject sẽ tự động giải quyết chúng và tạo ra các instance của tất cả các lớp được yêu cầu, làm việc theo cách của
nó xuyên suốt chuỗi các phụ thuộc để sau cùng nó có thể tạo ra một instance của kiểu mà bạn đã yêu cầu
Để biểu diễn tính năng này, tôi đã thêm một tập tin có tên là Discount.cs trong thư mục Models và sử dụng nó để định nghĩa một interface mới và một lớp sẽ triển khai nó, như trong Liệt kê 6-14
Liệt kê 6-14 Nội dung của tập tin Discount.cs
namespace EssentialTools.Models {
public interface IDiscountHelper {
decimal ApplyDiscount(decimal totalParam);
}
public class DefaultDiscountHelper : IDiscountHelper {
Trang 12public decimal ApplyDiscount(decimal totalParam) {
return (totalParam - (10m / 100m * totalParam));
Liệt kê 6-15 Thêm một phụ thuộc trong tập tin LinqValueCalculator.cs
using System.Collections.Generic;
using System.Linq;
namespace EssentialTools.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));
Tôi ràng buộc interface IDiscountHelper vào lớp triển khai với kernel của Ninject trong lớp NinjectDependencyResolver, hệt như tôi đã làm với IValueCalculator, như trong Liệt kê 6-16
Liệt kê 6-16 Ràng buộc một Interface khác vào triển khai của nó trong tập tin NinjectDependencyResolver.cs
Đặc tả Thuộc tính và Các Giá trị của Tham số trong Constructor
Trang 13Tôi có thể cấu hình các đối tượng mà Ninject tạo ra bằng cách cung cấp chi tiết về giá trị mà tôi muốn áp dụng cho các thuộc tính khi tôi ràng buộc interface vào triển khai của nó Để biểu diễn tính năng này, tôi đã sửa đổi lớp DefaultDiscountHelper
để nó định nghĩa một thuộc tính DiscountSize, mà tôi dùng để tính mức giảm giá, như trong Liệt kê 6-17
Liệt kê 6-17 Thêm một Thuộc tính trong tập tin Discount.cs
namespace EssentialTools.Models {
public interface IDiscountHelper {
decimal ApplyDiscount(decimal totalParam);
}
public class DefaultDiscountHelper : IDiscountHelper {
public decimal DiscountSize { get; set; }
public decimal ApplyDiscount(decimal totalParam) {
return (totalParam - (DiscountSize / 100m * totalParam));
Liệt kê 6-18 Chú ý rằng tôi cung cấp tên của thuộc tính để thiết lập như một giá trị chuỗi
Liệt kê 6-18 Sử dụng Phương thức WithPropertyValue của Ninject trong tập tin NinjectDependencyResolver.cs
}
Tôi không cần phải thay đổi các ràng buộc khác, cũng không cần thay đổi cách tôi sử dụng phương thức Get để có được một instance của lớp ShoppingCart Giá trị của thuộc tính được thiết lập sau khi xây dựng lớp DefaultDiscountHelper,
và có tác dụng giảm một nửa tổng giá trị của các item Hình 6-3 cho thấy kết quả của sự thay đổi này
H ình 6-3 Tác dụng của việc áp dụng giảm giá thông qua một thuộc tính khi giải quyết chuỗi phụ thuộc
Nếu bạn có nhiều hơn một giá trị thuộc tính mà bạn cần phải thiết lập , bạn có thể gọi dây chuyền phương thức WithPropertyValue để bao quát tất cả chúng Tôi có thể làm điều tương tự với các tham số constructor Liệt kê 6-
19 cho thấy lớp DefaultDiscounterHelper đã được làm lại để mức giảm giá được truyền qua như là một tham
số constructor
Trang 14Liệt kê 6-19 Sử dụng một Thuộc tính của Constructor trong tập tin Discount.cs
namespace EssentialTools.Models {
public interface IDiscountHelper {
decimal ApplyDiscount(decimal totalParam);
}
public class DefaultDiscountHelper : IDiscountHelper {
public decimal discountSize;
public DefaultDiscountHelper(decimal discountParam) {
discountSize = discountParam;
}
public decimal ApplyDiscount(decimal totalParam) {
return (totalParam - (discountSize / 100m * totalParam));
Sử dụng Ràng buộc Có điều kiện
Ninject hỗ trợ một số các phương thức ràng buộc có điều kiện cho phép tôi để đặc tả các lớp mà kernel nên sử dụng để đáp ứng các yêu cầu dành cho các interface cụ thể Để biểu diễn tính năng này, tôi đã thêm một tập tin mới vào thư mục Models của project mẫu có tên là FlexibleDiscountHelper.cs, bạn có thể xem nội dung củ nó trong Liệt kê 6-21
Liệt kê 6-21 Nội dung của tập tin FlexibleDiscountHelper.cs
namespace EssentialTools.Models {
public class FlexibleDiscountHelper : IDiscountHelper {
public decimal ApplyDiscount(decimal totalParam) {
decimal discount = totalParam > 100 ? 70 : 25;
return (totalParam - (discount / 100m * totalParam));
}
Trang 15}
}
Lớp FlexibleDiscountHelper áp dụng các giảm giá khác nhau dựa trên độ lớn của tổng số Bây giờ tôi có một loạt các lớp có thể chọn để triển khai interface IDiscountHelper, tôi có thể thay đổi phương thức AddBindings của NinjectDependencyResolver để báo với Ninject khi tôi muốn sử dụng mỗi một trong số chúng, như trong Liệt
Bảng 6-2 Các Phương thức Ràng buộc Có điều kiện của Ninject
Phương thức Tác dụng
When(predicate) Ràng buộc được sử dụng khi vị từ—một biểu thức lambda—trả về t rue
WhenClassHas<T>() Ràng buộc được sử dụng khi lớp đang bị inject được chú thích với thuộc tính có kiểu được quy định bởi T
WhenInjectedInto<T>() Ràng buộc được sử dụng khi lớp đang bị inject vào thuộc kiểu T
Thiết lập Phạm vi của Đối tượng
Tính năng cuối cùng của Ninject giúp quản lý vòng đời của đối tượng mà Ninject tạo ra để phù hợp với nhu cầu của ứng dụng của bạn Mặc định, Ninject sẽ tạo một instance mới của mọi đối tượng cần để giải quyết tất cả phụ thuộc mỗi khi bạn yêu cầu một đối tượng
Để minh chứng cho những gì sẽ xảy ra, Tôi đã sửa đổi constructor cho lớp LinqValueCalculator class để nó xuất một thông báo ra cửa sổ Output của Visual Studio khi một instance mới được tạo ra, như trong Liệt kệ 6-23
Liệt kê 6-23 Thêm một Constructor trong tập tin LinqValueCalculator.cs
using System.Collections.Generic;
using System.Linq;
namespace EssentialTools.Models {
public class LinqValueCalculator : IValueCalculator {
private IDiscountHelper discounter;
private static int counter = 0;
public LinqValueCalculator(IDiscountHelper discountParam) {
discounter = discountParam;
Trang 16System.Diagnostics.Debug.WriteLine(
string.Format("Instance {0} created", ++counter));
}
public decimal ValueProducts(IEnumerable<Product> products) {
return discounter.ApplyDiscount(products.Sum(p => p.Price)); }
}
}
Lớp System.Diagnostics.Debug chứa một số phương thức có thể được sử dụng để xuất ra các thông điệp debugging và tôi thấy chúng hữu ích khi kết code để xem chúng hoạt động như thế nào Tiếc thay, tôi đã đủ già dặn nên các debugger không phức tạp khi tôi bắt đầu viết code và tôi thấy mình như trở lại với kiến thức cơ bản khi nói đến debugging
Trong Liệt kê 6-24, tôi đã sửa đổi controller Home để nó yêu cầu hai triển khai của interface IValueCalculator từ Ninject
Liệt kê 6-24 Sử dụng nhiều Instance của Lớp Calculator trong tập tin HomeController.cs
sẽ muốn tạo một instance mới cho mỗi yêu cầu HTTP mà nền tảng ASP.NET nhận được Ninject cho phép bạn kiểm soát vòng đời
của các đối tượng bạn tạo bằng một tính năng có gòi là một phạm vi, được thể hiện bằng một lời gọi phương thức khi thiết lập ràng
buộc giữa một interface và kiểu triển khai của nó Trong Liệt kê 6-25, bạn có thể thấy cách tôi áp dụng phạm vi hữu ích nhất cho các
ứng dụng MVC Framework: phạm vi yêu cầu cho lớp LinqValueCalculator trong NinjectDependencyResolver
Liệt kê 6-25 Sử dụng Phạm vi Yêu cầu trong tập tin NinjectDependencyResolver.cs
public class NinjectDependencyResolver : IDependencyResolver {
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernelParam) {
kernel = kernelParam;
AddBindings();
Trang 17public object GetService(Type serviceType) {
kernel.Bind<IDiscountHelper>()
.To<DefaultDiscountHelper> ().WithConstructorArgument("discountParam", 50M);
kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>() WhenInjectedInto<LinqValueCalculator>();
Bảng 6-3 Các Phương thức Phạm vi của Ninject
Kiểm thử Đơn vị với Visual Studio
Trong cuốn sách này, Tôi sử dụng hỗ trợ kiểm thử đơn vị tích hợp kèm với Visual Studio, nhưng vẫn có các gói kiểm thử đơn vị NET khác có sẵn Thông dụng nhất có lẻ là NUnit, nhưng tất cả các gói kiểm thử hầu hết đều như nhau Lý do tôi chọn công cụ kiểm thử củaVisual Studio test tools là vì tôi thích sự tích hợp với toàn bộ phần còn lại của IDE
Để biểu diễn hỗ trợ kiểm thử đơn vị của Visual Studio, tôi đã thêm một triển khai mới của interface IDiscountHelper vào project mẫu Tạo một tập tin mới trong thư mục Models có tên là MinimumDiscountHelper.cs và đảm bảo rằng nội dung của nó giống như trong Liệt kê 6-26
Liệt kê 6-26 Nội dung của tập tin MinumumDiscountHelper.cs
using System;
namespace EssentialTools.Models {
public class MinimumDiscountHelper : IDiscountHelper {