摘要
可以使用不同的模式向消費者類注入依賴項,向構造器里注入依賴項是其中一種。有一些遵循的模式用來注冊依賴項,同時有一些需要避免的模式,因為他們經常導致不合乎需要的結果。這篇文章講述那些跟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 }