Ninject之旅之七:Ninject依賴注入


摘要

可以使用不同的模式向消費者類注入依賴項,向構造器里注入依賴項是其中一種。有一些遵循的模式用來注冊依賴項,同時有一些需要避免的模式,因為他們經常導致不合乎需要的結果。這篇文章講述那些跟Ninject功能相關的模式和反模式。然而,全面的介紹可以在Mark Seemann的書《Dependency Injection in .NET》中找到。

1、構造函數注入

構造函數時推薦的最常用的向一個類注冊依賴項的模式。一般來說,這種模式應該經常被用作主要的注冊模式,除非我們不得不使用其他的模式。在這個模式中,需要在構造函數中引入所有類依賴項列表。

問題是如果一個類有多於一個的構造函數會怎么樣。盡管Ninject選擇構造函數的策略是可以訂制的,他默認的行為是選擇那個有更多可以被Ninject解析的參數的構造函數。

因此在下面的例子中,盡管第二個構造函數有更多的參數,如果Ninject不能解析IService2,他將調用第一個構造函數。如果IService1也不能被解析,他將調用默認構造函數。如果兩個依賴項都被注冊了可以被解析,Ninject將調用第二個構造函數,因為他有更多的參數。

 1 public class Consumer
 2 {
 3   private readonly IService1 dependency1;
 4   private readonly IService2 dependency2;
 5   public Consumer(IService1 dependency1)
 6   {
 7     this.dependency1 = dependency1;
 8   }
 9   public Consumer(IService1 dependency1, IService2 dependency2)
10   {
11     this.dependency1 = dependency1;
12     this.dependency2 = dependency2;
13   }
14 }

如果上一個類中有另一個構造函數也有兩個可以被解析的參數,Ninject將拋出一個ActivationException異常,通知多個構造函數有相同的優先級。

有兩種方式可以重載默認的行為,顯示地選擇調用哪一個構造函數。第一種方式是在綁定中指出需要的構造函數:

Bind<Consumer>().ToConstructor(arg => new Consumer(arg.Inject<IService1>()));

另一種方式是在需要的構造函數中使用[Inject]特性:

1 [Inject]
2 public Consumer(IService1 dependency1)
3 {
4   this.dependency1 = dependency1;
5 }

在上面的例子中,我們再第一個構造函數中使用了[Inject]特性,顯式地指定在初始化類中注入依賴項時調用的構造函數。盡管第二個構造函數有更多的參數,按照Ninject默認的策略會選擇第二個構造函數。

注意在多個構造函數中同時使用這個特性時會產生ActivationException異常。

2、初始化方法和屬性注入
除了構造函數注入之外,Ninject支持通過初始化方法和屬性setter依賴注入。我們可以通過[Inject]特性指定任意多的需要的方法和屬性來注入依賴項。

盡管依賴項在類一初始化的時候就被注入,但是不能預計依賴項注入的順序。下面的例子演示如何指定一個屬性的注入:

1 [Inject]
2 public IService Service
3 {
4   get { return dependency; }
5   set { dependency = value; }
6 }

下面的例子使用注入方法注冊依賴項:

1 [Inject]
2 public void Setup(IService dependency)
3 {
4     this.dependency = dependency;
5 }

注意只有公有成員和公有構造函數才可以被注入,甚至internal的成員都被忽視除非Ninject配置成可以注冊非公有成員。

在構造函數注入中,構造函數是單一的點,在這個點上,類被初始化后就可以使用它的所有的依賴項。但是,如果我們使用初始化方法,依賴項通過多個點以無法預期的順序被注入。因此,不能知道在哪個方法中,所有的依賴項都已經被注入可以使用了。為了解決這個問題,Ninject提供了IInitializable接口。這個接口有一個IInitialize方法,一旦所有的依賴項都被注入,將調用這個方法:

 1 public class Consumer : IInitializable
 2 {
 3     private IService1 dependency1;
 4     private IService2 dependency2;
 5     [Inject]
 6     public IService Service1
 7     {
 8         get { return dependency1; }
 9         set { dependency1 = value; }
10     }
11     [Inject]
12     public IService Service2
13     {
14       get { return dependency2; }
15       set { dependency2 = value; }
16     }
17     public void Initialize()
18     {
19       // Consume all dependencies here
20     }
21 }

盡管Ninject支持使用屬性和方法注入,構造函數注入應該是優先的方式。首先,構造函數注入使類更好的重用性,因為所有的類依賴項的列表是可見的。在初始化屬性或方法里,類的使用者需要研究類的所有的成員或者瀏覽了類說明文檔后(如果有的話),才能發現他的依賴項。
當使用構造函數注入的時候,類的初始化更容易。因為所有的依賴項在同一時刻被注入,我們可以很容易地在相同的地方使用這些依賴項。正如我們在前面的例子中看到的那樣,在構造函數注入的場景中,注入的字段可能是只讀的。因為只讀字段只能在構造函數中被初始化,我們需要將他改成可寫的,才可以使用初始化方法和屬性對他進行注入。這將導致潛在的修改字段可讀寫屬性的問題。
3、服務定位器

服務定位器是Martin Fowler介紹的一種很有爭議的設計模式。盡管在一些特定的場景中可能有用,他一般被認為是一種反模式,需要盡可能避免。如果我們不屬性這個模式,Ninject很容易地被誤用成服務定位器。下面的例子示范誤用Ninject kernal成一個服務定位器而不是一個DI容器:

 1 public class Consumer
 2 {
 3   public void Consume()
 4   {
 5     var kernel = new StandardKernel();
 6     var depenency1 = kernel.Get<IService1>();
 7     var depenency2 = kernel.Get<IService2>();
 8     ...
 9   }
10 }

前面的代碼有兩個重大的缺點。第一個是盡管我們使用一個DI容器,但是我們不可能一直使用DI。這個類跟Ninject kernal綁在一起,然而Ninject kernal並不真的是這個類的依賴項。這個類以及他所有預期的調用類將總是不必要的依賴於kernal對象和Ninject類庫。在另一方面,真正的類依賴項(IService1和IService2)對於這個類卻不可見,降低了可重用性。即使我們按照下面的方式修改這個類的設計,這個問題仍然存在:

 1 public class Consumer
 2 {
 3   private readonly IKernel kernel;
 4   public Consumer(IKernel kernel)
 5   {
 6     this.kernel = kernel;
 7   }
 8   public void Consume()
 9   {
10     var depenency1 = kernel.Get<IService1>();
11     var depenency2 = kernel.Get<IService2>();
12     ...
13   }
14 }

前面的類仍舊依賴於Ninject類庫,然后他不必要這樣,他真正的依賴項仍然對調用者不可見。可以很容易地使用構造函數注入模式重構:

1 public Consumer(IService1 dependency1, IService2 dependency2)
2 {
3   this.dependency1 = dependency1;
4   this.dependency2 = dependency2;
5 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM