為什麼需要依賴注入
在[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的綁定方式分為:一般綁定、指定值綁定、自我綁定、派生類綁定和條件綁定。這樣分類有點牽強,隻是為了本文的寫作需要和友善讀者閱讀而分,并不是官方的分類。
1、一般綁定
在前文的示例中用Bind和To方法把一個接口綁定到實作該接口的類,這屬于一般的綁定。通過前文的示例相信大家已經掌握了,在這就不再累述。
2、指定值綁定
我們知道,通過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();
}
運作結果:
運作結果是6.4,說明沒有打折,即調用的是計算方法是IterativeValueCalculator的ValueProducts方法。可見,Ninject會查找最比對的綁定,如果沒有找到條件綁定,則使用預設綁定。在條件綁定中,除了WhenInjectedInto方法,還有When和WhenClassHas等方法,朋友們可以在使用的時候再慢慢研究。
在ASP.NET MVC中使用Ninject
本文用控制台應用程式示範了Ninject的使用,但要把Ninject內建到ASP.NET MVC中還是有點複雜的。首先要做的事就是建立一個繼承System.Web.Mvc.DefaultControllerFactory的類,MVC預設使用這個類來建立Controller類的執行個體(後續博文會專門講這個)。代碼如下:
using System;
using Ninject;
using System.Web.Mvc;
using System.Web.Routing;
namespace MvcApplication1 {
public class NinjectControllerFactory : DefaultControllerFactory {
private IKernel ninjectKernel;
public NinjectControllerFactory() {
ninjectKernel = new StandardKernel();
AddBindings();
}
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) {
return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType);
}
private void AddBindings() {
// 在這添加綁定,
// 如:ninjectKernel.Bind<IProductRepository>().To<FakeProductRepository>();
}
}
}
現在暫時不解釋這段代碼,大家都看懂就看,看不懂就過,隻要知道在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容器,可以使得開發和測試更簡單、更高效。
代碼位址:http://download.csdn.net/detail/yl3456/7072889
作者:Liam Wang
出處:http://www.cnblogs.com/willick/