[ASP.NET
MVC 小牛之路]04 - 依賴注入(DI)和Ninject
本文目錄:
為什麼需要依賴注入
在[ASP.NET MVC 的小奶牛之路系列中。在理解MVC模式文章中,我們提到MVC的一個重要特徵是關注點分離(separation of concerns)。我們希望應用程式的各部分元件盡可能多的相互獨立、盡可能少的相互依賴。
我們的理想情況是:一個元件可以不知道也可以不關心其他的元件,但透過提供的公開介面卻可以實現其他元件的功能呼叫。這種情況就是所謂的鬆散耦合。
舉個簡單的例子。我們要為商品訂製一個「高級」的價錢計算器LinqValueCalculator,這個計算器需要實作IValueCalculator介面。如下程式碼所示:
public interface IValueCalculator { decimal ValueProducts(params Product[] products); } public class LinqValueCalculator : IValueCalculator { public decimal ValueProducts(params Product[] products) { return products.Sum(p => p.Price); } }
Product類別和前兩篇部落格文章中用到的是一樣的。現在有個購物車ShoppingCart類,它需要有一個能計算購物車內商品總價錢的功能。但購物車本身沒有計算的功能,因此,購物車要嵌入一個計算器組件,這個計算器組件可以是LinqValueCalculator組件,但不一定是LinqValueCalculator組件(以後購物車升級,可能會嵌入別的更高級的計算器)。那我們可以這樣定義購物車ShoppingCart類別:
1 public class ShoppingCart { 2 //计算购物车内商品总价钱 3 public decimal CalculateStockValue() { 4 Product[] products = { 5 new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, 6 new Product {Name = "苹果", Category = "水果", Price = 4.9M}, 7 new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, 8 new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} 9 }; 10 IValueCalculator calculator = new LinqValueCalculator(); 11 12 //计算商品总价钱 13 decimal totalValue = calculator.ValueProducts(products); 14 15 return totalValue; 16 } 17 }
ShoppingCart類別是透過IValueCalculator介面(而不是透過LinqValueCalculator)來計算商品總價錢的。如果以後購物車升級需要使用更進階的計算器,那麼只需要改變第10行程式碼中new後面的物件(即把LinqValueCalculator換掉),其他的程式碼都不用變動。這樣就實現了一定的鬆散耦合。這時三者的關係如下圖所示:
這個圖說明,ShoppingCart類別既依賴IValueCalculator介面又依賴LinqValueCalculator類別。這樣就有個問題,用現實世界的話來講就是,如果嵌入在購物車內的計算器組件壞了,會導致整個購物車不能正常工作,豈不是要把整個購物車要換掉!最好的方法是將計算器組件和購物車完全獨立開來,這樣不管哪個組件壞了,只要換對應的組件即可。也就是我們要解決的問題是,要讓ShoppingCart組件和LinqValueCalculator組件完全斷開關係,而依賴注入這種設計模式就是為了解決這個問題。
什麼是依賴注入
上面實現的部分鬆散耦合顯然不是我們所需要的。我們所需要的是,在一個類別內部,不透過創建物件的實例而能夠獲得某個實現了公開介面的物件的參考。這種“需要”,就稱為DI(依賴注入,Dependency Injection),和所謂的IoC(控制反轉,Inversion of Control )是一個意思。
DI是一種透過介面實現鬆散耦合的設計模式。初學者可能會好奇網上為什麼有那麼多技術文章對DI這個東西大興其筆,是因為DI對於基於幾乎所有框架下,要高效開發應用程序,它都是開發者必須要有的一個重要的理念,包括MVC開發。它是解耦的一個重要手段。
DI模式可分為兩個部分。一是移除對元件(上面範例中的LinqValueCalculator)的依賴,二是透過類別的建構子(或類別的Setter存取器)來傳遞實作了公開介面的元件的參考。如下面程式碼所示:
public class ShoppingCart { IValueCalculator calculator; //构造函数,参数为实现了IValueCalculator接口的类的实例 public ShoppingCart(IValueCalculator calcParam) { calculator = calcParam; } //计算购物车内商品总价钱 public decimal CalculateStockValue() { Product[] products = { new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, new Product {Name = "苹果", Category = "水果", Price = 4.9M}, new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} }; //计算商品总价钱 decimal totalValue = calculator.ValueProducts(products); return totalValue; } }
這樣我們就徹底斷開了ShoppingCart和LinqValueCalculator之間的依賴關係。某個實作了IValueCalculator介面的類別(範例中的LinqValueCalculator)的實例參考作為參數,傳遞給ShoppingCart類別的建構子。但是ShoppingCart類別不知道也不關心這個實作了IValueCalculator介面的類別是什麼,更沒有責任去操作這個類別。 這時我們可以用下圖來描述ShoppingCart、LinqValueCalculator和IValueCalculator之間的關係:
在程序运行的时候,依赖被注入到ShoppingCart,这个依赖就是,通过ShoppingCart构造函数传递实现了IValueCalculator接口的类的实例引用。在程序运行之前(或编译时),ShoppingCart和任何实现IValueCalculator接口的类没有任何依赖关系。(注意,程序运行时是有具体依赖关系的。)
注意,上面示例使用的注入方式称为“构造注入”,我们也可以通过属性来实现注入,这种注入被称为“setter 注入”,就不举例了,朋友们可以看看T2噬菌体的文章依赖注入那些事儿来对DI进行更多的了解。
由于经常会在编程时使用到DI,所以出现了一些DI的辅助工具(或叫DI容器),如Unity和Ninject等。由于Ninject的轻量和使用简单,加上本人只用过Ninject,所以本系列文章选择用它来开发MVC应用程序。下面开始介绍Ninject,但在这之前,先来介绍一个安装Ninject需要用到的插件-NuGet。
使用NuGet安装库
NuGet 是一种 Visual Studio 扩展,它能够简化在 Visual Studio 项目中添加、更新和删除库(部署为程序包)的操作。比如你要在项目中使用Log4Net这个库,如果没有NuGet这个扩展,你可能要先到网上搜索Log4Net,再将程序包的内容解压缩到解决方案中的特定位置,然后在各项目工程中依次添加程序集引用,最后还要使用正确的设置更新 web.config。而NuGet可以简化这一切操作。例如我们在讲依赖注入的项目中,若要使用一个NuGet库,可直接右击项目(或引用),选择“管理NuGet程序包”(VS2010下为“Add
Library Package Reference”),如下图:
在弹出如下窗口中选择“联机”,搜索“Ninject”,然后进行相应的操作即可:
在本文中我们只需要知道如何使用NuGet来安装库就可以了。NuGet的详细使用方法可查看MSDN文档:使用 NuGet 管理项目库。
使用Ninject的一般步骤
在使用Ninject前先要创建一个Ninject内核对象,代码如下:
class Program { static void Main(string[] args) { //创建Ninject内核实例 IKernel ninjectKernel = new StandardKernel(); } }
使用Ninject内核对象一般可分为两个步骤。第一步是把一个接口(IValueCalculator)绑定到一个实现该接口的类(LinqValueCalculator),如下:
... //绑定接口到实现了该接口的类 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator<(); ...
这个绑定操作就是告诉Ninject,当接收到一个请求IValueCalculator接口的实现时,就返回一个LinqValueCalculator类的实例。
第二步是用Ninject的Get方法去获取IValueCalculator接口的实现。这一步,Ninject将自动为我们创建LinqValueCalculator类的实例,并返回该实例的引用。然后我们可以把这个引用通过构造函数注入到ShoppingCart类。如下代码所示:
... // 获得实现接口的对象实例 IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); // 创建ShoppingCart实例并注入依赖 ShoppingCart cart = new ShoppingCart(calcImpl); // 计算商品总价钱并输出结果 Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); ...
Ninject的使用的一般步骤就是这样。该示例可正确输出如下结果:
但看上去Ninject的使用好像使得编码变得更加烦琐,朋友们会问,直接使用下面的代码不是更简单吗:
... IValueCalculator calcImpl = new LinqValueCalculator(); ShoppingCart cart = new ShoppingCart(calcImpl); Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); ...
的确,对于单个简单的DI,用Ninject确实显得麻烦。但如果添加多个复杂点的依赖关系,使用Ninject则可大大提高编码的工作效率。
Ninject如何提高编码效率
当我们请求Ninject创建某个类型的实例时,它会检查这个类型和其它类型之间的耦合关系。如果存在依赖关系,那么Ninject会根据依赖处理理它们,并创建所有所需类的实例。为了解释这句话和说明使用Ninject编码的便捷,我们再创建一个接口IDiscountHelper和一个实现该接口的类DefaultDiscountHelper,代码如下:
//折扣计算接口 public interface IDiscountHelper { decimal ApplyDiscount(decimal totalParam); } //默认折扣计算器 public class DefaultDiscountHelper : IDiscountHelper { public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (1m / 10m * totalParam)); } }
IDiscounHelper接口声明了ApplyDiscount方法,DefaultDiscounterHelper实现了该接口,并定义了打9折的ApplyDiscount方法。然后我们可以把IDiscounHelper接口作为依赖添加到LinqValueCalculator类中。代码如下:
public class LinqValueCalculator : IValueCalculator { private IDiscountHelper discounter; public LinqValueCalculator(IDiscountHelper discountParam) { discounter = discountParam; } public decimal ValueProducts(params Product[] products) { return discounter.ApplyDiscount(products.Sum(p => p.Price)); } }
LinqValueCalculator类添加了一个用于接收IDiscountHelper接口的实现的构造函数,然后在ValueProducts方法中调用该接口的ApplyDiscount方法对计算出的商品总价钱进行打折处理,并返回折后总价。
到这,我们先来画个图理一理ShoppingCart、LinqValueCalculator、IValueCalculator以及新添加的IDiscountHelper和DefaultDiscounterHelper之间的关系:
以此,我们还可以添加更多的接口和实现接口的类,接口和类越来越多时,它们的关系图看上去会像一个依赖“链”,和生物学中的分子结构图差不多。
按照前面说的使用Ninject的“二个步骤”,现在我们在Main中的方法中编写用于计算购物车中商品折后总价钱的代码,如下所示:
1 class Program { 2 static void Main(string[] args) { 3 IKernel ninjectKernel = new StandardKernel(); 4 5 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 6 ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>(); 7 8 IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); 9 ShoppingCart cart = new ShoppingCart(calcImpl); 10 Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); 11 Console.ReadKey(); 12 } 13 }
输出结果:
代码一目了然,虽然新添加了一个接口和一个类,但Main方法中只增加了第6行一句代码,获取实现IValueCalculator接口的对象实例的代码不需要做任何改变。
定位到代码的第8行,这一行代码,Ninject为我们做的事是:
当我们需要使用IValueCalculator接口的实现时(通过Get方法),它便为我们创建LinqValueCalculator类的实例。而当创建LinqValueCalculator类的实例时,它检查到这个类依赖IDiscountHelper接口。于是它又创建一个实现了该接口的DefaultDiscounterHelper类的实例,并通过构造函数把该实例注入到LinqValueCalculator类。然后返回LinqValueCalculator类的一个实例,并赋值给IValueCalculator接口的对象(第8行的calcImpl)。
总之,不管依赖“链”有多长有多复杂,Ninject都会按照上面这种方式检查依赖“链”上的每个接口和实现接口的类,并自动创建所需要的类的实例。在依赖“链”越长越复杂的时候,更能显示使用Ninject编码的高效率。
Ninject的绑定方式
我个人将Ninject的绑定方式分为:一般绑定、指定值绑定、自我绑定、派生类绑定和条件绑定。这样分类有点牵强,只是为了本文的写作需要和方便读者阅读而分,[b]并不是官方的分类。[/b]
1、一般绑定
在前文的示例中用Bind和To方法把一个接口绑定到实现该接口的类,这属于一般的绑定。通过前文的示例相信大家已经掌握了,在这就不再累述。
2、[b]指定值绑定[/b]
我们知道,通过Get方法,Ninject会自动帮我们创建我们所需要的类的实例。但有的类在创建实例时需要给它的属性赋值,如下面我们改造了一下的DefaultDiscountHelper类:
public class DefaultDiscountHelper : IDiscountHelper { public decimal DiscountSize { get; set; } public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (DiscountSize / 10m * totalParam)); } }
给DefaultDiscountHelper类添加了一个DiscountSize属性,实例化时需要指定折扣值(DiscountSize属性值),不然ApplyDiscount方法就没意义。而实例化的动作是Ninject自动完成的,怎么告诉Ninject在实例化类的时候给某属性赋一个指定的值呢?这时就需要用到参数绑定,我们在绑定的时候可以通过给WithPropertyValue方法传参的方式指定DiscountSize属性的值,如下代码所示:
public static void Main() { IKernel ninjectKernel = new StandardKernel(); ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); ninjectKernel.Bind<IDiscountHelper>() .To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 5M); IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); ShoppingCart cart = new ShoppingCart(calcImpl); Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); Console.ReadKey(); }
只是在Bind和To方法后添加了一个WithPropertyValue方法,其他代码都不用变,再一次见证了用Ninject编码的高效。
WithPropertyValue方法接收了两个参数,一个是属性名(示例中的"DiscountSize"),一个是属性值(示例中的5)。运行结果如下:
如果要给多个属性赋值,则可以在Bind和To方式后添加多个WithPropertyValue(,)方法。
我们还可以在类的实例化的时候为类的构造函数传递参数。为了演示,我们再把DefaultDiscountHelper类改一下:
public class DefaultDiscountHelper : IDiscountHelper { private decimal discountRate; public DefaultDiscountHelper(decimal discountParam) { discountRate = discountParam; } public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (discountRate/ 10m * totalParam)); } }
显然,DefaultDiscountHelper类在实例化的时候必须给构造函数传递一个参数,不然程序会出错。和给属性赋值类似,只是用的方法是WithConstructorArgument(,),绑定方式如下代码所示:
... ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); ninjectKernel.Bind<IDiscountHelper>() .To< DefaultDiscountHelper>().WithConstructorArgument("discountParam", 5M); ...
同样,只需要更改一行代码,其他代码原来怎么写还是怎么写。如果构造函数有多个参数,则需在Bind和To方法后面加上多个WithConstructorArgument即可。
3.自我绑定
Niject的一个非常好用的特性就是自绑定。当通过Bind和To方法绑定好接口和类后,可以直接通过ninjectKernel.Get()来获得一个类的实例。
在前面的几个示例中,我们都是像下面这样来创建ShoppingCart类实例的:
... IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); ShoppingCart cart = new ShoppingCart(calcImpl); ...
其实有一种更简单的定法,如下:
... ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); ...
这种写法不需要关心ShoppingCart类依赖哪个接口,也不需要手动去获取该接口的实现(calcImpl)。当通过这句代码请求一个ShoppingCart类的实例的时候,Ninject会自动判断依赖关系,并为我们创建所需接口对应的实现。这种方式看起来有点怪,其实中规中矩的写法是:
... ninjectKernel.Bind<ShoppingCart>().ToSelf(); ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); ...
这里有自我绑定用的是ToSelf方法,在本示例中可以省略该句。但用ToSelf方法自我绑定的好处是可以在其后面用WithXXX方法指定构造函数参数、属性等等的值。
4.派生类绑定
通过一般绑定,当请求一个接口的实现时,Ninject会帮我们自动创建实现接口的类的实例。我们说某某类实现某某接口,也可以说某某类继承某某接口。如果我们把接口当作一个父类,是不是也可以把父类绑定到一个继承自该父类的子类呢?我们来实验一把。先改造一下ShoppingCart类,给它的CalculateStockValue方法改成虚方法:
public class ShoppingCart { protected IValueCalculator calculator; protected Product[] products; //构造函数,参数为实现了IEmailSender接口的类的实例 public ShoppingCart(IValueCalculator calcParam) { calculator = calcParam; products = new[]{ new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, new Product {Name = "苹果", Category = "水果", Price = 4.9M}, new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} }; } //计算购物车内商品总价钱 public virtual decimal CalculateStockValue() { //计算商品总价钱 decimal totalValue = calculator.ValueProducts(products); return totalValue; } }
再添加一个ShoppingCart类的子类:
public class LimitShoppingCart : ShoppingCart { public LimitShoppingCart(IValueCalculator calcParam) : base(calcParam) { } public override decimal CalculateStockValue() { //过滤价格超过了上限的商品 var filteredProducts = products.Where(e => e.Price < ItemLimit); return calculator.ValueProducts(filteredProducts.ToArray()); } public decimal ItemLimit { get; set; } }
然后把父类ShoppingCart绑定到子类LimitShoppingCart:
public static void Main() { IKernel ninjectKernel = new StandardKernel(); ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>() .WithPropertyValue("DiscountSize", 5M); //派生类绑定 ninjectKernel.Bind<ShoppingCart>().To<LimitShoppingCart>() .WithPropertyValue("ItemLimit", 3M); ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); Console.ReadKey(); }
运行结果:
从运行结果可以看出,cart对象调用的是子类的CalculateStockValue方法,证明了可以把父类绑定到一个继承自该父类的子类。通过派生类绑定,当我们请求父类的时候,Ninject自动帮我们创建一个对应的子类的实例,并将其返回。由于抽象类不能被实例化,所以派生类绑定在使用抽象类的时候非常有用。
5.条件绑定
当一个接口有多个实现或一个类有多个子类的时候,我们可以通过条件绑定来指定使用哪一个实现或子类。为了演示,我们给IValueCalculator接口再添加一个实现,如下:
public class IterativeValueCalculator : IValueCalculator { public decimal ValueProducts(params Product[] products) { decimal totalValue = 0; foreach (Product p in products) { totalValue += p.Price; } return totalValue; } }
IValueCalculator接口现在有两个实现:IterativeValueCalculator和LinqValueCalculator。我们可以指定,如果是把该接口的实现注入到LimitShoppingCart类,那么就用IterativeValueCalculator,其他情况都用LinqValueCalculator。如下所示:
public static void Main() { IKernel ninjectKernel = new StandardKernel(); ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>() .WithPropertyValue("DiscountSize", 5M); //派生类绑定 ninjectKernel.Bind<ShoppingCart>().To<LimitShoppingCart>() .WithPropertyValue("ItemLimit", 3M); //条件绑定 ninjectKernel.Bind<IValueCalculator>() .To<IterativeValueCalculator>().WhenInjectedInto<LimitShoppingCart>(); ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); Console.ReadKey(); }
运行结果:
在ASP.NET MVC中使用Ninject
本文用控制台应用程序演示了Ninject的使用,但要把Ninject集成到ASP.NET MVC中还是有点复杂的。首先要做的事就是创建一个继承System.Web.Mvc.DefaultControllerFactory的类,MVC默认使用这个类来创建Controller类的实例(后续博文会专门讲这个)。代码如下:
NinjectControllerFactory
现在暂时不解释这段代码,大家都看懂就看,看不懂就过,只要知道在ASP.NET MVC中使用Ninject要做这么一件事就行。
添加完这个类后,还要做一件事,就是在MVC框架中注册这个类。一般我们在Global.asax文件中的Application_Start方法中进行注册,如下所示:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory()); }
注册后,MVC框架就会用NinjectControllerFactory类去获取Cotroller类的实例。在后续博文中会具体演示如何在ASP.NET MVC中使用Ninject,这里就不具体演示了,大家知道需要做这么两件事就行。
虽然我们前面花了很大功夫来学习Ninject就是为了在MVC中使用这样一个NinjectControllerFactory类,但是了解Ninject如何工作是非常有必要的。理解好了一种DI容器,可以使得开发和测试更简单、更高效。
以上就是[ASP.NET MVC 小牛之路]04 - 依赖注入(DI)和Ninject的内容,更多相关内容请关注PHP中文网(www.php.cn)!