依賴注入(DI)和Ninject

爲何須要依賴注入

在[ASP.NET MVC 小牛之路]系列的理解MVC模式文章中,咱們提到MVC的一個重要特徵是關注點分離(separation of concerns)。咱們但願應用程序的各部分組件儘量多的相互獨立、儘量少的相互依賴。html

咱們的理想狀況是:一個組件能夠不知道也能夠不關心其餘的組件,但經過提供的公開接口卻能夠實現其餘組件的功能調用。這種狀況就是所謂的鬆耦合web

舉個簡單的例子。咱們要爲商品定製一個「高級」的價錢計算器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組件徹底斷開關係,而依賴注入這種設計模式就是爲了解決這種問題。ide

什麼是依賴注入

上面實現的部分鬆耦合顯然並非咱們所須要的。咱們所須要的是,在一個類內部,不經過建立對象的實例而可以得到某個實現了公開接口的對象的引用。這種「須要」,就稱爲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的綁定方式分爲:通常綁定、指定值綁定、自我綁定、派生類綁定和條件綁定。這樣分類有點牽強,只是爲了本文的寫做須要和方便讀者閱讀而分,並非官方的分類

一、通常綁定

在前文的示例中用Bind和To方法把一個接口綁定到實現該接口的類,這屬於通常的綁定。經過前文的示例相信你們已經掌握了,在這就再也不累述。

二、指定值綁定

咱們知道,經過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類的實例(後續博文會專門講這個)。代碼以下:

  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容器,可使得開發和測試更簡單、更高效。

相關文章
相關標籤/搜索