.net mvc笔记4_依赖注入
一、Building Loosely Coupled Components
MVC模式最重要的特点就是关注点分离。我们希望应用中的组件能尽可能的独立,相互之间即使有依赖也要在我们的控制之下。
在理想情况下,每个组件对其他组件都是透明的,只通过抽象接口来交互。这就被叫做loose coupling(松散的耦合),这可以使测试和修改变得容易。
一个简单的例子可以使事情进入状态。如果我们正在写一个叫做MyEmailSender的组件来发送e-mail,我们就需要实现一个接口定义出发送邮件需要的所有公共函数,我们可以叫它IEmailSender。
我们应用中的任何其他组件需要发送邮件的时候——假设一个重置密码的PasswordResetHelper——只能通过接口来发送邮件。在PasswordResetHelper和MyEmailSender之间没有直接依赖,如下图所示。
Figure 4-7. Using interfaces to decouple components
当然,并不是所有的关系都需要用interface来去耦合,这取决于应用有多复杂,以及需要什么样的测试和维护。
1、Using Dependency Injection
interfaces帮助我们解耦组件,但我们仍面临一个问题——C#不能提供内建的方式来轻松创建对象实现interface,只能创建concrete component实例。例如:
1 Listing 4-3. Instantiating Concrete Classes to Get an Interface Implementation 2 public class PasswordResetHelper { 3 public void ResetPassword() { 4 IEmailSender mySender = new MyEmailSender(); //直接创建了实现接口的实际类的实例 5 ...call interface methods to configure e-mail details... 6 mySender.SendEmail(); 7 } 8 }
PasswordResetHelper类通过IEmailSender 接口(interface)配置和发送邮件,但是要创建实现interface的对象,它需要创建MyEmailSender的实例。
实际上,上面这种做法让事情变糟了,现在PasswordResetHelper同时依赖于IEmailSender和MyEmailSender,如下图
Figure 4-8. Components are tightly coupled after all
我们需要的是得到一个对象可以实现给定的接口而不需要直接创建实现它的对象。解决这个问题的方案就是依赖注入(dependency injection, DI),也被称为控制反转(inversion of control, IoC)。
依赖注入是一个设计模式,用来实现松散耦合(实现添加IEmailSender接口到我们例子中的松散耦合)。它是MVC开发中的一个重要概念。
依赖注入模式有两部分,第一是从我们的组件中移出concrete classes上的任何依赖——本例子中就是要在PasswordResetHelper上移除对MyEmailSender的任何依赖
。我们把所需接口的实现放到构造函数中(通过构造函数的参数来注入),如表4-4:
1 Listing 4-4. Removing Dependencies from the PasswordResetHelper Class 2 public class PasswordResetHelper { 3 private IEmailSender emailSender; 4 public PasswordResetHelper(IEmailSender emailSenderParam) { //用参数来将接口的具体实现注入 5 emailSender = emailSenderParam; 6 } 7 public void ResetPassword() { 8 ...call interface methods to configure e-mail details... 9 emailSender.SendEmail(); 10 } 11 }
我们打破了PasswordResetHelper和MyEmailSender之间的依赖。PasswordResetHelper构造函数需要一个对象来实现IEmailSender接口,但它不知道是哪一个对象,也不需要为创建它负责。
依赖在运行时刻才被注入PasswordResetHelper。这意味着实现了IEmailSender接口的类的实例将被创建并被传递给PasswordResetHelper的构造函数,这些都发生在PasswordResetHelper被实例化的过程中。在PasswordResetHelper和它的接口之间没有编译时刻的依赖。
(注意) PasswordResetHelper类使用的是构造函数来注入的依赖,这被称为构造函数依赖(constructor injection)。我们也允许依赖被注入到公共属性(public property),这被叫做setter injection。
因为依赖在运行时刻处理,我们可以决定在运行应用时使用哪一个接口实现。我们可以选择不同的e-mail providers,也可选择注入一个mock实现来进行测试。
2、Using a Dependency Injection Container
我们已经解决了我们的依赖问题:在运行时刻把依赖注入到我们类的构造函数。但我们还有一个问题需要解决:不在我们应用中别的地方创建依赖,如何实现concrete implementation of interfaces(接口具体实现)的实例化。
答案是“依赖注入”容器(DI container),也被叫做反转控制容器(IoC container)。这是一个扮演了中间人角色的组件,在依赖需求(如PasswordResetHelper)和对依赖的具体实现(如MyEmailSender)之间扮演中间人的角色。
我们把我们应用中要用到的接口和抽象类型注册为DI container,并告诉它哪一个具体的类会被实例化来满足依赖。所以,我们应该将IEmailSender接口注册到容器中,并指定任何时候当IEmailSender的实现被需要时,MyEmailSender的实例就将被创建。任何时候当我们需要一个IEmailSender时(如创建PasswordResetHelper的实例时),我们就到DI container中找到并给出注册的该接口默认的具体实现——本例中就是MyEmailSender。
我们不需要自己创建DI container。这里有一些非常好的开源和免费授权的实现可以得到。我们喜欢的一个叫做Ninject,你可以从www.ninject.org得到更多细节。
Tip 微软创建了它自己的DI container,叫做Unity。我们使用Ninject,因为我们喜欢它,它展示了使用MVC时混合和匹配工具的能力。如果你想得到关于Unity的信息,可以访问unity.codeplex.com
DI container的角色看起来简单琐碎,实际上不是这样。一个好的DI container,比如Ninject,有很多聪明的特性:
(1) Dependency chain resolution:如果你请求一个有自己的依赖的组件(例如,通过构造函数的参数),容器也能满足这些依赖。所以,如果MyEmailSender类的构造函数需要INetworkTransport接口的实现,DI container会实例化这个接口的默认实现,并传递给MyEmailSender的构造函数,最后返回结果作为IEmailSender的默认实现。
(2) Object life-cycle management:如果你请求的组件不止一次,你每次得到的是一个新的实例还是一个相同的实例?好的DI container可以让你配置组件的life cycle,允许你从预定义选项中选择,包括singleton(每次用相同的实例),transient(每次用新的实例),instance-per-thread,instance-per-HTTP-request,instance-from-a-pool,等。
(3) Configuration of constructor parameter values:例如,假设实现INetworkTransport接口的构造函数需要名为serverName的字符串,你可以在DI container配置中设置。这个粗糙但简单的配置系统消除了你需要传递连接字符串、服务器地址等等需要。
你也可以自己尝试写自己的DI container。我们认为这是一个非常不错的学习C#和.NET反射机制(reflection)的实践项目,如果你有大量的时间。
二、Using Ninject
Ninject是我们比较喜欢的DI容器。它简单、优雅、且易用。还有不少更完善的选择,但我们喜欢Ninject最小配置工作的方式。我们考虑以模式为起点,这并不是定律,而是我们发现用Ninject很容易定制我们的DI。如果你不喜欢Ninject,我们建议你用Unity,这是微软的一个DI工具。
DI思想。再次重申,该思想是让我们的MVC应用程序组件实现松耦合,实现的方法是结合接口与DI。清单6-1演示了一个接口,它表示了统计一些产品总价的功能,以及这个接口的具体实现。
Listing 6-1. The Class, the Interface, and Its Implementation
1 public class Product { 2 public int ProductID { get; set; } 3 public string Name { get; set; } 4 public string Description { get; set; } 5 public decimal Price { get; set; } 6 public string Category { set; get; } 7 } 8 public interface IValueCalculator { //IValueCalculator接口 9 decimal ValueProducts(params Product[] products); 10 } 11 public class LinqValueCalculator : IValueCalculator { //接口的具体实现 12 public decimal ValueProducts(params Product[] products) { 13 return products.Sum(p => p.Price); 14 } 15 }
IValueCalculator接口定义了一个方法,它以一个或多个Product对象为参数,返回数值。我们在LinqValueCalculator类中实现了这个接口,它使用LINQ扩展方法Sum巧妙地得到各Product对象Price值的总和。我们现在需要生成一个使用IValueCalculator的类,而且这是为DI设计的。这个类如列表6-2所示。
Listing 6-2. Consuming the IValueCalculator Interface
1 public class ShoppingCart { 2 private IValueCalculator calculator; 3 public ShoppingCart(IValueCalculator calcParam) {//构造函数,通过参数注入IValueCalculator一个实现的实例 4 calculator = calcParam; 5 } 6 public decimal CalculateStockValue() { 7 // define the set of products to sum 8 Product[] products = { 9 new Product() { Name = "Kayak", Price = 275M}, 10 new Product() { Name = "Lifejacket", Price = 48.95M}, 11 new Product() { Name = "Soccer ball", Price = 19.50M}, 12 new Product() { Name = "Stadium", Price = 79500M} 13 }; 14 // calculate the total value of the products 15 decimal totalValue = calculator.ValueProducts(products); //使用接口中提供的方法 16 // return the result 17 return totalValue; 18 } 19 }
这是一个很简单的例子。类ShoppingCart的构造函数接收了一个IValueCalculator的实现作为参数,以此来为注入依赖(DI)作准备。CalculateStockValue方法生成一个Product对象的数组,然后调用IValueCalculator接口中的ValueProducts来获得各对象的总价,以此作为返回结果。我们已经成功地去掉了ShoppingCart类与LinqValueCalculator类的耦合,如图6-1所示,它描述了这四个简单类型之间的关系。
Figure 6-1. The relationships among four simple types
ShoppingCart类和LinqValueCalculator类都依赖于接口IValueCalculator,但ShoppingCart与LinqValueCalculator没有直接关系,事实上,它甚至不知道LinqValueCalculator的存在。我们可以修改LinqValueCalculator的实现,甚或用一个全新IValueCalculator实现来代替,ChoppingCart类依然一无所知。
(注意)Product类与所有其它三个类型都有一个直接关系。对此我们不必着急。Product等于域模型类型,而且我们期望这种类与应用程序的其余部分是强耦合的。如果我们不是在建立MVC应用程序,我们也许会对此采取不同的观点,并去掉Product耦合。
我们的目标是能够生成ShoppingCart实例,并把IValueCalculator类的一个实现作为构造函数参数进行注入。这就是是Ninject所扮演的角色。但在我们能够示范Ninject之前,我们需要在Visual Studio中进行安装。
1、Creating the Project
我们打算从一个简单的控制台应用程序开始。在Visual Studio中用控制台模板生成一个新项目,控制台项目可以在Windows模板节区中找到。我们将此项目称为NinjectDemo,名字并不重要。生成如前面列表6-1和6-2所示的接口和类。我们将所有内容放到一个单一的C#代码文件中。
2、Adding Ninject
要把Ninject添加到你的项目,你需要Visual Studio Library Package管理器。在解决方案窗口中右击你的项目,并从弹出菜单中选择“Add Package Library Reference”,以打开“Add Package Library Reference”对话框。在对话框的左侧点击“在线”,然后在右上角的搜索框中输入Ninject。于是会出现一些条目
你将看到几个Ninject相关的包,但从名字和描述应该可以看出哪个是核心Ninject库 — 其它条目应该是将Ninject与不同开发框架和工具集成的扩展。
点击条目右边的“Install”按钮将该库添加到你的项目。你将在解决方案窗口中看到打开的References文件夹,以及下载并被添加到你项目引用中的Ninject程序集。
(注意)在已经安装了Ninject包之后,如果项目编译还有问题,请选择“项目”菜单中的“项目属性”菜单项,将“目标框架”的设置从“.NET Framework 4 Client Profile”改为“.NET Framework 4”。客户端轮廓(Client Profile)是一种瘦型安装,它忽略了Ninject所依赖的一个库。
3、Getting Started with Ninject
为了准备使用Ninject,我们需要生成一个Ninject内核的实例,这是我们用来与Ninject进行通信的对象。我们将在Program类中完成这一工作,Program类是Visual Studio作为控制台应用程序项目模板部件所生成的。这是具有Main方法的类。生成内核如列表6-3所示。
Listing 6-3. Preparing a Ninject Kernel
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using Ninject; //添加 6 7 namespace NinjectDemo 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 IKernel ninjectKernel = new StandardKernel(); 14 } 15 } 16 }
一旦你已经生成了这个内核,与Ninject进行工作有两个阶段。第一阶段是把你想与之进行通信的类型与你已经生成的接口进行绑定。本例中,我们想告诉Ninject,当它接收到一个实现IValueCalculator的请求时,它应该生成并返回LinqValueCalculator类的一个实例。我们用该IKernel接口中定义的Bind和To方法来完成这件事,如列表6-4所示。
Listing 6-4. Binding a Type to Ninject
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 IKernel ninjectKernel = new StandardKernel(); 6 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 7 } 8 }
黑体语句把IValueCalculator接口绑定到LinqValueCalculator类的实现上。通过把接口作为Bind方法的泛型类型参数(generic type parameter),我们由此指定了我们想要注册的接口,再把它对应的具体实现作为To方法的泛型类型参数。第二阶段是用Ninject的Get方法来生成一个实现这个接口的对象,并把它传递给ShoppingCart类的构造器,如列表6-5所示。
Listing 6-5. Instantiating an Interface Implementation via Ninject
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using Ninject; //添加 6 7 namespace NinjectDemo 8 { 9 public class Product 10 { 11 public int ProductID { get; set; } 12 public string Name { get; set; } 13 public string Description { get; set; } 14 public decimal Price { get; set; } 15 public string Category { set; get; } 16 } 17 public interface IValueCalculator //接口 18 { 19 decimal ValueProducts(params Product[] products); 20 } 21 public class LinqValueCalculator : IValueCalculator //对接口的实现 22 { 23 public decimal ValueProducts(params Product[] products) 24 { 25 return products.Sum(p => p.Price); 26 } 27 } 28 29 public class ShoppingCart 30 { 31 private IValueCalculator calculator; 32 public ShoppingCart(IValueCalculator calcParam) 33 { 34 calculator = calcParam; 35 } 36 public decimal CalculateStockValue() 37 { 38 // define the set of products to sum 39 Product[] products = { 40 new Product() { Name = "Kayak", Price = 275M}, 41 new Product() { Name = "Lifejacket", Price = 48.95M}, 42 new Product() { Name = "Soccer ball", Price = 19.50M}, 43 new Product() { Name = "Stadium", Price = 79500M} 44 }; 45 // calculate the total value of the products 46 decimal totalValue = calculator.ValueProducts(products);//使用接口提供的方法 47 // return the result 48 return totalValue; 49 } 50 } 51 52 class Program 53 { 54 static void Main(string[] args) 55 { 56 IKernel ninjectKernel = new StandardKernel(); 57 58 // 将接口IValueCalculator的实现绑定到LinqValueCalculator上 59 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 60 61 // get the interface implementation 62 // 通过get来指定要使用的接口,生成实例calcImpl 63 IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); 64 65 // Create the instance of ShoppingCart and inject the dependency 66 // 将实例calcImpl注入到ShoppingCart中。 67 ShoppingCart cart = new ShoppingCart(calcImpl); 68 69 // perform the calculation and write out the result 70 Console.WriteLine("Total:{0:c}", cart.CalculateStockValue()); 71 } 72 } 73 }
我们把想要实现的接口指定为Get方法的泛型类型参数。Ninject通过我们已经定义的绑定进行查找,明白我们已经把IValueCalculator绑定到LinqValueCalculator,于是为我们生成了一个新实例。然后我们把这个实现注入到ShoppingCart类的构造器中,并调用CalculateStockValue方法,它又反过来调用在这个接口中定义的方法。这段代码所得到的结果如下:
Total: $79,843.45
你会觉得奇怪的是,我们没有必要花力气去安装Ninject也可以自己轻易地创建LinqValueCalculator的实例,像这样:
ShoppingCart cart = new ShoppingCart(new LinqValueCalculator());
是的,对于这种简单的例子,使用Ninject看上去反而需要更多功夫,但当我们开始把复杂性添加到我们的应用程序时,Ninject很快会变得方便。在以下几小节中,我们将建立复杂的例子,并演示Ninject的一些不同特性。
3、Creating Chains of Dependency
当我们要求Ninject生成一个类型时,它检查这个类型与其它类型之间的耦合。如果有附加的依赖性,Ninject会解析这些依赖性,并生成所需要的所有类的实例。为了演示这一特性,我们生成一个新的接口和实现这个接口的类,如列表6-6所示。
Listing 6-6. Defining a New Interface and Implementation
1 public interface IDiscountHelper { 2 decimal ApplyDiscount(decimal totalParam); 3 } 4 public class DefaultDiscountHelper : IDiscountHelper { 5 public decimal ApplyDiscount(decimal totalParam) { 6 return (totalParam - (10m / 100m * totalParam)); 7 } 8 }
接口IDiscountHelper中定义了ApplyDiscount方法,返回decimal类型的折扣后的值。类DefaultDiscountHelper实现了这个接口,应用固定折扣值10%。我们随后可以把接口IDiscountHelper添加为LinqValueCalculator的一个依赖,如列表6-7所示。
Listing 6-7. Adding a Dependency in the LinqValueCalculator Class
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace _6_6ChainsOfDependency 7 { 8 public class Product 9 { 10 public int ProductID { get; set; } 11 public string Name { get; set; } 12 public string Description { get; set; } 13 public decimal Price { get; set; } 14 public string Category { set; get; } 15 } 16 17 public interface IDiscountHelper //接口1 18 { 19 decimal ApplyDiscount(decimal totalParam); 20 } 21 22 public class DefaultDiscountHelper : IDiscountHelper 23 { 24 public decimal ApplyDiscount(decimal totalParam) 25 { 26 return (totalParam - (10m / 100m * totalParam)); 27 } 28 } 29 30 public interface IValueCalculator //接口2 31 { 32 decimal ValueProducts(params Product[] products); 33 } 34 35 public class LinqValueCalculator : IValueCalculator 36 { 37 private IDiscountHelper discounter; 38 public LinqValueCalculator(IDiscountHelper discountParam)//接口1通过构造函数的参数注入接口2的实现 39 { 40 discounter = discountParam; 41 } 42 public decimal ValueProducts(params Product[] products) 43 { 44 return discounter.ApplyDiscount(products.Sum(p => p.Price));//在接口2的实现中使用接口1的实例discounter 45 } 46 } 47 48 class Program 49 { 50 static void Main(string[] args) 51 { 52 } 53 } 54 }
新添加的这个类LinqValueCalculator的构造函数以IDiscountHelper接口的一个实现为参数,这个参数会被用到ValueProducts方法中算出各个对象价格累加和后的折扣值。接下来我们把IDiscountHelper接口绑定到带有Ninject内核的实现类上,正如我们对IValueCalculator所做的那样,如列表6-8所示。
Listing 6-8. Binding Another Interface to Its Implementation
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using Ninject; //添加 6 7 namespace _6_6ChainsOfDependency 8 { 9 public class Product 10 { 11 public int ProductID { get; set; } 12 public string Name { get; set; } 13 public string Description { get; set; } 14 public decimal Price { get; set; } 15 public string Category { set; get; } 16 } 17 18 public interface IDiscountHelper //接口1 19 { 20 decimal ApplyDiscount(decimal totalParam); 21 } 22 23 public class DefaultDiscountHelper : IDiscountHelper //接口1的实现 24 { 25 public decimal ApplyDiscount(decimal totalParam) 26 { 27 return (totalParam - (10m / 100m * totalParam)); 28 } 29 } 30 31 public interface IValueCalculator //接口2 32 { 33 decimal ValueProducts(params Product[] products); 34 } 35 36 public class LinqValueCalculator : IValueCalculator //接口2的实现 37 { 38 private IDiscountHelper discounter; 39 public LinqValueCalculator(IDiscountHelper discountParam) //接口2的实现中使用了接口1 40 { 41 discounter = discountParam; 42 } 43 public decimal ValueProducts(params Product[] products) 44 { 45 return discounter.ApplyDiscount(products.Sum(p => p.Price)); 46 } 47 } 48 49 public class ShoppingCart 50 { 51 private IValueCalculator calculator; 52 public ShoppingCart(IValueCalculator calcParam) 53 { 54 calculator = calcParam; 55 } 56 public decimal CalculateStockValue() 57 { 58 // define the set of products to sum 59 Product[] products = { 60 new Product() { Name = "Kayak", Price = 275M}, 61 new Product() { Name = "Lifejacket", Price = 48.95M}, 62 new Product() { Name = "Soccer ball", Price = 19.50M}, 63 new Product() { Name = "Stadium", Price = 79500M} 64 }; 65 // calculate the total value of the products 66 decimal totalValue = calculator.ValueProducts(products); 67 // return the result 68 return totalValue; 69 } 70 } 71 72 73 class Program 74 { 75 static void Main(string[] args) 76 { 77 IKernel ninjectKernel = new StandardKernel(); 78 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 79 ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>(); 80 81 // get the interface implementation 82 IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); 83 ShoppingCart cart = new ShoppingCart(calcImpl); 84 Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); 85 } 86 } 87 }
列表6-8也使用了我们生成的类和我们用Ninject绑定的接口。我们不需要对之前的IValueCalculator接口作任何修改。
当IValueCalculator被请求时,Ninject知道我们是想实例化LinqValueCalculator类。它会考察这个类,并发现它依赖于一个可以解析的接口。Ninject会生成一个DefaultDiscountHelper实例,把它注入到LinqValueCalculator类的构造器中,并以IValuCalculator作为返回结果。Ninject会以这种方式检查它要实例化的每个依赖性类,而不管这个依赖性链有多长或有多复杂。
4、Specifying Property and Parameter Values
在我们把接口绑定到它的实现时,我们可以提供属性细节来配置Ninject生成的类。我们修改了DefaultDiscountHelper类,于是它暴露了一个方便的属性,以便指定折扣的大小,如列表5-9所示。
Listing 6-9. Adding a Property to an Implementation Class
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的类型时,我们可以用WithPropertyValue方法来设置DefaultDiscountHelper类中的DiscountSize属性,如列表6-10所示。
Listing 6-10. Using the Ninject WithPropertyValue Method
1 ... 2 IKernel ninjectKernel = new StandardKernel(); 3 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 4 ninjectKernel.Bind<IDiscountHelper>() 5 .To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 50M); 6 ...
注意,我们必须提供一个字符串值作为要设置的属性名。我们不需要修改任何其它绑定,也不需要修改我们使用Get方法的方式来获得ShoppingCart方法的一个实例。该属性值会按照DefaultDiscountHelper的结构进行设置,并起到了半价的效果。
如果你有不止一个属性值需要设置,你可以接连调用WithPropertyValue方法来涵盖所有这些属性。我们可以用构造器参数做同样的事。列表6-11演示了重写的DefaultDiscounter类,以使折扣大小作为构造器参数来进行传递。
Listing 6-11. Using a Constructor Property in an Implementation Class
public class DefaultDiscountHelper : IDiscountHelper { private decimal discountRate; public DefaultDiscountHelper(decimal discountParam) { //构造函数 discountRate = discountParam; } public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (discountRate/ 100m * totalParam)); } }
为了用Ninject绑定这个类,我们用WithConstructorArgument方法来指定构造器参数的值,如列表6-12所示。
Listing 6-12. Binding to a Class that Requires a Constructor Parameter
1 ... 2 IKernel ninjectKernel = new StandardKernel(); 3 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 4 ninjectKernel.Bind<IDiscountHelper>() 5 .To< DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M); 6 ...
这种技术允许你把一个值注入到构造函数中。当然,我们可以接连调用这些方法来提供多值、混合值,并与依赖性匹配。Ninject会推断出我们的需要并因此而生成它。
5、Using Self-Binding
把Ninject集成到你的代码中的一个有用的特性是自身绑定,自身绑定是在Ninject内核请求一个具体类(并因此实例化)的地方进行的。这样做似乎有点古怪,但它意味着我们不需要手工地执行初始DI,在前面的几个示例中,我们都是像下面这样来创建ShoppingCart类实例的:
1 IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); 2 ShoppingCart cart = new ShoppingCart(calcImpl);
取而代之的是我们可以简单地请求ShoppingCart的一个实例,让Ninject去挑出对IValueCalculator类的依赖。列表6-13演示了自身绑定的使用。
Listing 6-13. Using Ninject Self-Binding
1 ... 2 ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); 3 ...
我们不需要做任何准备来自身绑定一个类。当我们请求一个还没有进行绑定的具体类时,Ninject假设这就是我们所需要的。(通过自绑定,绑定好接口和类之后,可以直接通过ninjectKernel.Get<ShoppingCart>()来获得一个类的实例。)本例子中是ShoppingCart cart = ninjectKernel.Get<ShoppingCart>();调用。这种写法不需要关心ShoppingCart类依赖哪个接口,也不需要手动去获取该接口的实现(calcImpl)。当通过这句代码请求一个ShoppingCart类的实例的时候,Ninject会自动判断依赖关系,并为我们创建所需接口对应的实现。这种方式看起来有点怪,规矩的的写法是:
1 ... 2 ninjectKernel.Bind<ShoppingCart>().ToSelf(); 3 ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); 4 ...
有些DI纯粹论者不喜欢自身绑定,但我们喜欢。它帮助处理应用程序中最初的DI工作,并把各种事物,包括具体对象,纳入Ninject范围。如果我们花时间去注册一个自身绑定类型,我们可以使用对接口有效的特性,就像指定构造器参数和属性值一样。为了注册一个自身绑定,我们使用ToSelf方法,如列表6-14所示。
Listing 6-14. Self-Binding a Concrete Type
1 ninjectKernel.Bind<ShoppingCart>().ToSelf().WithParameter("<parameterName>", <paramvalue>);
这个例子把ShoppingCart绑定到自身,然后调用WithParameter方法为一个(假想的)属性提供一个值。你也可以只对具体类进行自身绑定。
6、Binding to a Derived Type
虽然我们关注于接口(因为这是与MVC应用程序最相关的),但我们也可以用Ninject来绑定具体类。在前面的小节中,我们演示了如何把一个具体类绑定到自身,但我们也可以把一个具体类绑定到一个派生类。列表6-15演示了一个ShoppingCart类,它已作了修改以支持方便的派生,和一个派生类,LimitShoppingCart,它通过排除超过指定的限定价的所有条目的方法增强了父类功能。
(通过一般绑定,当请求一个接口的实现时,Ninject会帮我们自动创建实现接口的类的实例。我们说某某类实现某某接口,也可以说某某类继承某某接口。如果我们把接口当作一个父类,是不是也可以把父类绑定到一个继承自该父类的子类呢?我们来实验一把。先改造一下ShoppingCart类,给它的CalculateStockValue方法改成虚方法:)
Listing 6-15. Creating a Derived Shopping Cart Class
1 public class ShoppingCart { 2 protected IValueCalculator calculator; //calculator在子类中要用所以要定义为protected 3 protected Product[] products; 4 //构造函数,参数为接口IValueCalculator 5 public ShoppingCart(IValueCalculator calcParam) { 6 calculator = calcParam; 7 // define the set of products to sum 8 products = new[] { 9 new Product() { Name = "Kayak", Price = 275M}, 10 new Product() { Name = "Lifejacket", Price = 48.95M}, 11 new Product() { Name = "Soccer ball", Price = 19.50M}, 12 new Product() { Name = "Stadium", Price = 79500M} 13 }; 14 } 15 // 虚方法,计算购物车内商品总价 16 public virtual decimal CalculateStockValue() { 17 // calculate the total value of the products 18 // 计算总价,由IValueCalculator接口中的ValueProducts方法来计算 19 decimal totalValue = calculator.ValueProducts(products); 20 // return the result 21 return totalValue; 22 } 23 } 24 25 //定义一个ShoppingCart的子类 26 public class LimitShoppingCart : ShoppingCart 27 { 28 public LimitShoppingCart(IValueCalculator calcParam) 29 : base(calcParam) 30 { 31 // nothing to do here 32 } 33 //在子类中对父类的虚方法CalculateStockValue进行重载 34 // 过滤掉价格超过上限的商品 35 public override decimal CalculateStockValue() 36 { 37 // filter out any items that are over the limit 38 var filteredProducts = products 39 .Where(e => e.Price < ItemLimit); 40 // perform the calculation 41 return calculator.ValueProducts(filteredProducts.ToArray()); 42 } 43 public decimal ItemLimit { get; set; } 44 }
接下来,我们将父类ShoppingCart绑定到子类LimitShoppingCart上,这样,当我们通过Ninject来请求一个父类的实例时,该子类的实例就会被产生。如列表6-16
Listing 6-16. Binding a Class to a Derived Version
1 ... 2 ninjectKernel.Bind<ShoppingCart>() 3 .To<LimitShoppingCart>().WithPropertyValue("ItemLimit", 200M); 4 ...
下面是完整代码:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using Ninject; 6 7 namespace _6_16BindingToDerived 8 { 9 public class Product 10 { 11 public int ProductID { get; set; } 12 public string Name { get; set; } 13 public string Description { get; set; } 14 public decimal Price { get; set; } 15 public string Category { set; get; } 16 } 17 18 public interface IDiscountHelper //接口1 19 { 20 decimal ApplyDiscount(decimal totalParam); 21 } 22 23 public class DefaultDiscountHelper : IDiscountHelper //接口1的实现, 24 { 25 public decimal DiscountSize { get; set; } //带折扣属性 26 public decimal ApplyDiscount(decimal totalParam) 27 { 28 return (totalParam - (DiscountSize / 100m * totalParam)); 29 } 30 31 } 32 33 public interface IValueCalculator //接口2 34 { 35 decimal ValueProducts(params Product[] products); 36 } 37 38 public class LinqValueCalculator : IValueCalculator //接口2的实现 39 { 40 private IDiscountHelper discounter; 41 public LinqValueCalculator(IDiscountHelper discountParam) //接口2的实现中使用了接口1 42 { 43 discounter = discountParam; 44 } 45 public decimal ValueProducts(params Product[] products) 46 { 47 return discounter.ApplyDiscount(products.Sum(p => p.Price)); 48 } 49 } 50 51 public class ShoppingCart 52 { 53 protected IValueCalculator calculator; //calculator在子类中要用所以要定义为protected 54 protected Product[] products; 55 //构造函数,参数为接口IValueCalculator 56 public ShoppingCart(IValueCalculator calcParam) 57 { 58 calculator = calcParam; 59 // define the set of products to sum 60 products = new[] { 61 new Product() { Name = "Kayak", Price = 275M}, 62 new Product() { Name = "Lifejacket", Price = 48.95M}, 63 new Product() { Name = "Soccer ball", Price = 19.50M}, 64 new Product() { Name = "Stadium", Price = 79500M} 65 }; 66 } 67 // 虚方法,计算购物车内商品总价 68 public virtual decimal CalculateStockValue() 69 { 70 // calculate the total value of the products 71 // 计算总价,由IValueCalculator接口中的ValueProducts方法来计算 72 decimal totalValue = calculator.ValueProducts(products); 73 // return the result 74 return totalValue; 75 } 76 } 77 78 //定义一个ShoppingCart的子类 79 public class LimitShoppingCart : ShoppingCart 80 { 81 public LimitShoppingCart(IValueCalculator calcParam) 82 : base(calcParam) 83 { 84 // nothing to do here 85 } 86 //在子类中对父类的虚方法CalculateStockValue进行重载 87 // 过滤掉价格超过上限的商品 88 public override decimal CalculateStockValue() 89 { 90 // filter out any items that are over the limit 91 var filteredProducts = products 92 .Where(e => e.Price < ItemLimit); 93 // perform the calculation先过滤,过滤后的结果再计算总和 94 return calculator.ValueProducts(filteredProducts.ToArray()); 95 } 96 public decimal ItemLimit { get; set; } 97 } 98 99 class Program 100 { 101 static void Main(string[] args) 102 { 103 IKernel ninjectKernel = new StandardKernel(); 104 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 105 ninjectKernel.Bind<IDiscountHelper>() 106 .To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 10M); 107 //将基类绑定到派生类 108 ninjectKernel.Bind<ShoppingCart>().To<LimitShoppingCart>() 109 .WithPropertyValue("ItemLimit", 40M); 110 ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); 111 Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); 112 113 } 114 } 115 }
显示结果为:
17.55
大于40的被过滤掉,就只剩下Name = "Soccer ball", Price = 19.50M ,求和再减去10%的折扣,结果就为17.55。从运行结果可以看出,cart对象调用的是子类的CalculateStockValue方法,证明了可以把父类绑定到一个继承自该父类的子类。通过派生类绑定,当我们请求父类的时候,Ninject自动帮我们创建一个对应的子类的实例,并将其返回。由于抽象类不能被实例化,所以派生类绑定在使用抽象类的时候非常有用。
7、Using Conditional Binding
当一个接口有多个实现或一个类有多个子类的时候,我们可以通过条件绑定来指定使用哪一个实现或子类。为了演示,我们给IValueCalculator接口再添加一个实现,如下:
Listing 6-17. A New Implementation of the IValueCalculator
1 public class IterativeValueCalculator : IValueCalculator { 2 public decimal ValueProducts(params Product[] products) { 3 decimal totalValue = 0; 4 foreach (Product p in products) { 5 totalValue += p.Price; 6 } 7 return totalValue; 8 } 9 }
IValueCalculator接口现在有两个实现:IterativeValueCalculator和LinqValueCalculator。我们可以指定,如果是把该接口的实现注入到LimitShoppingCart类,那么就用IterativeValueCalculator,其他情况都用LinqValueCalculator。如下所示:
Listing 6-18. A Conditional Ninject Binding
1 ... 2 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 3 ninjectKernel.Bind<IValueCalculator>() 4 .To<IterativeValueCalculator>().WhenInjectedInto<LimitShoppingCart>(); 5 ...
下面为部分实现代码:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 IKernel ninjectKernel = new StandardKernel(); 6 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 7 ninjectKernel.Bind<IDiscountHelper>() 8 .To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 10M); 9 //将基类绑定到派生类 10 ninjectKernel.Bind<ShoppingCart>().To<LimitShoppingCart>() 11 .WithPropertyValue("ItemLimit", 40M); 12 //条件绑定 13 ninjectKernel.Bind<IValueCalculator>() 14 .To<IterativeValueCalculator>().WhenInjectedInto<LimitShoppingCart>(); 15 ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); 16 Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); 17 18 } 19 }
显示结果为:
19.50
因为有基类绑定到派生类,所以ShoppingCart在生成实例时由其派生类LimitShoppingCart来实现,又因为有条件绑定,依赖注入如果注入的是LimitShoppingCart,那么接口IValueCalculator就会被绑定到IterativeValueCalculator上来实现。所以最后显示的就是没有打折的结果19.5。
Table 6-1. Ninject Conditional Binding Methods
Method |
Effect |
When(predicate) |
Binding is
used when the predicate—a lambda expression—evaluates to true. |
WhenClassHas<T>() |
Binding is
used when the class being injected is annotated with the attribute whose type
is specified by T. |
WhenInjectedInto<T>() |
Binding is
used when the class being injected into is of type T (see the example in
Listing 6-18). |
三、Applying Ninject to ASP.NET MVC
我们已经用一个标准的Windows控制台应用程序,向你演示了Ninject的核心特性,但把Ninject与ASP.NET MVC集成并不是很容易的。第一步是要生成一个从System.Web.Mvc.DefaultControllerFactory派生而来的类。这是MVC默认地赖以生成控制器类实例的一个类。(在第14章,我们将向你演示如何用一个自定义的实现来替换这个默认的控制器生成器(控制器工厂))我们的实现叫做NinjectControllerFactory,如列表6-19所示。
Listing 6-19. The NinjectControllerFactory
1 using System; 2 using System.Web.Mvc; 3 using System.Web.Routing; 4 using Ninject; 5 using NinjectDemo.Models.Abstract; 6 using NinjectDemo.Models.Concrete; 7 8 namespace NinjectDemo.Infrastructure { 9 public class NinjectControllerFactory : DefaultControllerFactory { 10 private IKernel ninjectKernel; 11 public NinjectControllerFactory() { 12 ninjectKernel = new StandardKernel(); 13 AddBindings(); 14 } 15 protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { 16 return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); 17 } 18 private void AddBindings() { 19 // 在这添加绑定, 20 // 如:ninjectKernel.Bind<IProductRepository>().To<FakeProductRepository>(); 21 } 22 } 23 }
这个类生成了一个Ninject内核,并用它对通过GetControllerInstance方法产生的控制器类的请求进行服务。GetControllerInstance方法是由MVC框架在需要一个控制器对象时调用的。我们不需要用Ninject明确地绑定控制器。我们可以依靠其默认的自身绑定特性(default self-binding ),因为控制器是从System.Web.Mvc.Controller派生的。
AddBinding方法允许我们为存储库(repositories)和想保持松散耦合(loosely coupled)的组件添加其他Ninject绑定。我们也把这个方法作为一个机会,用来绑定需要附加构造参数或属性值的控制器类。
当我们添加了类后,还需要在MVC框架中注册这个类。一般我们在Global.asax文件中的Application_Start方法中进行注册,如下所示:
Listing 6-20. Registering the NinjectControllerFactory Class with the MVC Framework
1 protected void Application_Start() { 2 AreaRegistration.RegisterAllAreas(); 3 4 WebApiConfig.Register(GlobalConfiguration.Configuration); 5 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); 6 RouteConfig.RegisterRoutes(RouteTable.Routes); 7 8 ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory()); 9 }
注册后,MVC框架就会用NinjectControllerFactory类去获取Cotroller类的实例。而且Ninject将自动地把DI运用到控制器对象中。
你可以看到本例清单引用了IProductRepository、FakeProductRepository、Product等类型。我们已经生成了一个演示Ninject集成的简单的MVC应用程序,这些类型是本演示所需要的域模型类型和存储库类型。我们此刻不打算介入到这个项目中去,因为你将在下一章看到这些类的适当使用。但如果你对我们生成的这个例子感兴趣,你可以在这本书伴随的源代码下载中找到这个项目。
我们似乎对一个简单的集成类已经介绍得很多了,但我们认为这是你完全理解Ninject如何工作的基础。对DI容器的良好理解可以使开发和测试更简单容易。