[ASP.NET MVC 小牛之路]04 - 依賴注入(DI)和Ninject

來源:互聯網
上載者:User

[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的內容,更多相關內容請關注topic.alibabacloud.com(www.php.cn)!

  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.