摘要
如果我們已經知道了一個類所有的依賴項,在我們只需要依賴項的一個實例的場景中,在類的構造函數中引入一系列的依賴項是容易的。但是有些情況,我們需要在一個類里創建依賴項的多個實例,這時候Ninject注入就不夠用了。也有些情況,我們不知道一個消費者可能需要哪個服務,因為他可能在不同的場合下需要不同的服務,而且在創建類的時候實例化所有依賴項也不合理。這樣的情況,動態工廠可以幫忙。我們可以設計我們的類讓他依賴一個工廠,而不是依賴這個工廠能夠創建的對象。然后,我們能夠命令工廠去通過命令創建需要的類型和任意需要的數量。下面兩個例子解決上面兩個問題。Ninject動態工廠創建指定數量的依賴項和創建指定類型的依賴項。
例子:形狀工廠
附:代碼下載
在第一個例子中,我們將創建一個圖形動態庫。它包含一個ShapService類,提供一個AddShapes方法來給指定的ICanvas對象添加指定數量具體的IShape對象:
1 public void AddShapes(int circles, int squares, ICanvas canvas) 2 { 3 for (int i = 0; i < circles; i++) 4 { 5 var circle = new Circle(); 6 canvas.AddShap(circle); 7 } 8 for (int i = 0; i < squares; i++) 9 { 10 var square = new Square(); 11 canvas.AddShap(square); 12 } 13 }
傳統的方法是直接在AddShapes方法里創建新的Circle和Square類實例。然而,這個方法我們將ShapService類和具體的Circle和Square類耦合起來,這和DI原則相反。另外,通過參數引入這些依賴項不符合我們的需求,因為那樣一個形狀只注入一個實例,這樣不夠。為了解決這個問題,我們應該像下面首先創建一個簡單工廠接口:
1 public interface IShapeFactory 2 { 3 ICircle CreateCircle(); 4 ISquare CreateSquare(); 5 }
然后,我們可以引入這個工廠接口作為ShapeService類的依賴項。
1 public class ShapeService 2 { 3 private readonly IShapeFactory _factory; 4 5 public ShapeService(IShapeFactory factory) 6 { 7 this._factory = factory; 8 } 9 10 public void AddShapes(int circles, int squares, ICanvas canvas) 11 { 12 for (int i = 0; i < circles; i++) 13 { 14 var circle = _factory.CreateCircle(); 15 canvas.AddShap(circle); 16 } 17 for (int i = 0; i < squares; i++) 18 { 19 var square = _factory.CreateSquare(); 20 canvas.AddShap(square); 21 } 22 } 23 }
好消息是我們不需要擔心怎樣實現IShapeFactory。Ninject能夠動態地實現它,再注入這個實現的工廠到這個ShapeService類。我們只需要添加下面的代碼到我們類型注冊部分:
1 Bind<IShapeFactory>().ToFactory(); 2 Bind<ISquare>().To<Square>();
3 Bind<ICircle>().To<Circle>();
為了使用Ninject工廠,我們需要添加Ninject.Extensions.Factory動態庫的引用。可以通過NuGet添加,也可以通過從Ninject官方網站上下載。
記住工廠可以有需要的盡可能多的方法,每個方法可以返回任意需要的類型。這些方法可以有任意的名字,有任意數量的參數。唯一的限制是名字和參數類型必須跟具體類名字和構造函數參數的類型一致,但是跟他們的順序沒關系。甚至參數的數量都不需要一致,Ninject將試着解析那些沒有通過工廠接口提供的參數。
因此,如果具體Square類是下面這樣:
1 public class Square 2 { 3 public Square(Point startPoint, Point endPoint) 4 { ... } 5 }
這個IShapeFactory工廠接口就應該像下面這樣:
1 public interface IShapeFactory 2 { 3 ICircle CreateCircle(); 4 ISquare CreateSquare(Point startPoint, Point endPoint); 5 }
或者,CreateSquare方法可能像下面這樣:
1 ISquare CreateSquare(Point endPoint, Point startPoint);
這是Ninject動態工廠默認的行為。然而,通過創建自定義實例提供者,默認行為可以被重寫。后面的文章將要介紹這個。
對動態工廠注冊基於約定的綁定和常規的約定綁定稍微有點不同。不同在於,一旦我們選擇了程序集,我們應該選擇服務類型而不是組件,然后綁定他們到工廠。下面描述怎樣實現這兩個步驟。
- 選擇服務類型
使用下面的方法選擇一個抽象類或接口:
- SelectAllIncludingAbstractClasses(): 這個方法選擇所有的類,包括抽象類。
- SelectAllAbstractClasses(): 這個方法只選擇抽象類。
- SelectAllInterfaces(): 這個方法選擇所有接口。
- SelectAllTypes(): 這個方法選擇所有類型(類、接口、結構、共用體和原始類型)
下面的代碼綁定選擇的程序集下的所有接口到動態工廠:
1 kernel.Bind(x => x 2 .FromAssembliesMatching("factories") 3 .SelectAllInterfaces() 4 .BindToFactory());
2. 定義綁定生成器
使用下面的方法定義合適的綁定生成器:
- BindToFactory: 這個方法注冊映射的類型作為動態工廠。
- BindWith: 這個方法使用綁定生成器參數創建綁定。創建一個綁定生成器只是關於實現IBindingGenerator接口的問題
下面的例子綁定當前程序集中所有那些以Factory結尾的接口到動態工廠。
1 kernel.Bind(x => x 2 .FromThisAssembly() 3 .SelectAllInterfaces() 4 .EndingWith("Factory") 5 .BindToFactory());
例子:電信交換機
附:代碼下載
在下面的例子中,我們將為電信中心寫一個服務,這個服務返回指定交換機當前狀態信息。電信交換機生產於不同的廠家,可能提供不同的方法查詢狀態。一些支持TCP/IP協議通信,一些只是簡單地將狀態寫入一個文件。
先按下面這樣創建Switch類:
1 public class Switch 2 { 3 public string Name { get; set; } 4 public string Vendor { get; set; } 5 public bool SupportsTcpIp { get; set; } 6 }
收集交換機狀態像下面創建一個接口:
1 public interface IStatusCollector 2 { 3 string GetStatus(Switch @switch); 4 }
為兩種不同的交換機類型,我們需要對這個接口的兩個不同的實現。支持TCP/IP通信的交換機和那些不支持的。分別創建TcpStatusCollector類和FileStatusCollector類:
1 public class TcpStatusCollector : IStatusCollector 2 { 3 public string GetStatus(Switch @switch) 4 { 5 System.Console.WriteLine("TCP Get Status"); 6 return "TCP Status"; 7 } 8 } 9 10 public class FileStatusCollector : IStatusCollector 11 { 12 public string GetStatus(Switch @switch) 13 { 14 System.Console.WriteLine("File Get Status"); 15 return "File Status"; 16 } 17 }
我們還需要聲明一個可以創建者兩種具體StatusCollector實例的工廠接口:
1 public interface IStatusCollectorFactory 2 { 3 IStatusCollector GetTcpStatusCollector(); 4 IStatusCollector GetFileStatusCollector(); 5 }
最后是SwitchService類:
1 public class SwitchService 2 { 3 private readonly IStatusCollectorFactory factory; 4 5 public SwitchService(IStatusCollectorFactory factory) 6 { 7 this.factory = factory; 8 } 9 10 public string GetStatus(Switch @switch) 11 { 12 IStatusCollector collector; 13 if (@switch.SupportsTcpIp) 14 { 15 collector = factory.GetTcpStatusCollector(); 16 } 17 else 18 { 19 collector = factory.GetFileStatusCollector(); 20 } 21 return collector.GetStatus(@switch); 22 } 23 }
這個SwitchService類將絕不會創建一個FileStatusCollector實例,如果所有給定的交換機都支持TCP/IP。按這種方法,SwitchService類只注入他真實需要的依賴項,而不是所有他可能需要的依賴項。
IStatusCollectorFactory有兩個工廠方法,兩個都返回相同的類型。現在,Ninject這個工廠的實現如何理解怎樣解析IStatusCollector?魔法在於工廠方法的名字。無論何時工廠方法的名字以Get開頭,它指明這個類型將用名稱綁定來解析,類型名稱就是方法名后面那一串。例如,如果工廠方法名稱是GetXXX,這個工廠將試着去找一個名稱為XXX的綁定。因此,這個例子的類型注冊段應該像下面這樣:
1 Kernel.Bind(x => x.FromThisAssembly() 2 .SelectAllInterfaces() 3 .EndingWith("Factory") 4 .BindToFactory()); 5 6 Kernel.Bind(x => x.FromThisAssembly() 7 .SelectAllClasses() 8 .InheritedFrom<IStatusCollector>() 9 .BindAllInterfaces() 10 .Configure((b, comp) => b.Named(comp.Name)));
第一個約定綁定那些名稱以Factory結尾的接口到工廠。
第二個為所有的IStatusCollector的實現注冊名稱綁定,按這種方式,每個綁定用他的組件名稱命名。它等同於下面單獨的兩行綁定:
1 Kernel.Bind<IStatusCollector>().To<TcpStatusCollector>().Named("TcpStatusCollector"); 2 Kernel.Bind<IStatusCollector>().To<FileStatusCollector>().Named("FileStatusCollector");
然而,使用這樣的單獨綁定依賴於字符串名稱,這很容易出錯,這些關系可能很容易被拼寫錯誤打斷。有另一種特別為這種情況設計的單獨綁定命名的方式,當引用了Ninject.Extensions.Factory時才可用。我們可以使用NamedLikeFactoryMethod這個幫助方法而不用Named幫助方法來為一個工廠綁定命名:
1 Kernel.Bind<IStatusCollector>().To<TcpStatusCollector>().NamedLikeFactoryMethod((IStatusCollectorFactory f) => f.GetTcpStatusCollector()); 2 Kernel.Bind<IStatusCollector>().To<FileStatusCollector>().NamedLikeFactoryMethod((IStatusCollectorFactory f) => f.GetFileStatusCollector());
他意思是說我們在用工廠方法建議的名稱定義一個名稱綁定。
請注意使用約定經常是首選的方式。
自定義實例提供者
動態工廠不直接實例化請求的類型。然而,它使用另一個稱為實例提供者的對象(不要把他跟提供者混淆)來創建一個類型的實例。一些關於工廠方法的信息提供給了這個實例提供者。基於哪一個實例提供者應該來解析請求對象,這些信息包含方法名稱、它的返回類型和它的參數。如果一個工廠沒有賦給一個自定義實例提供者,它將使用它默認的實例提供者,名稱是StandardInstanceProvider。我們可以在注冊的時候賦給一個自定義實例提供者到一個工廠,像下面這樣:
1 Kernel.Bind(x => x.FromThisAssembly() 2 .SelectAllInterfaces() 3 .EndingWith("Factory") 4 .BindToFactory(() => new MyInstanceProvider()));
為了使Ninject接受一個類作為一個實例提供者,實現IInstanceProvider接口的類就足夠了。然而,更簡單的方法是繼承StandardInstanceProvider類並重載相應的成員。
下面的代碼顯示如何定義一個實例提供者,從NamedAttribute得到綁定名稱,而不是從方法名稱:
1 public class NameAttributeInstanceProvider : StandardInstanceProvider 2 { 3 protected override string GetName(System.Reflection.MethodInfo methodInfo, object[] arguments) 4 { 5 var nameAttribute = methodInfo 6 .GetCustomAttributes(typeof(NamedAttribute), true) 7 .FirstOrDefault() as NamedAttribute; 8 if (nameAttribute != null) 9 { 10 return nameAttribute.Name; 11 } 12 return base.GetName(methodInfo, arguments); 13 } 14 }
使用自定義實例提供者,我們能夠選擇任意名稱作為我們的工廠名稱,然后使用一個特性來指定請求的綁定名稱。
由於Ninject的NamedAttribute特性不能運用在方法上,我們需要創建我們自己的特性:
1 public class BindingNameAttribute : Attribute 2 { 3 public BindingNameAttribute(string name) 4 { 5 this.Name = name; 6 } 7 public string Name { get; set; } 8 }
NameAttributeInstanceProvider改為下面這樣:
1 public class NameAttributeInstanceProvider : StandardInstanceProvider 2 { 3 protected override string GetName(System.Reflection.MethodInfo methodInfo, object[] arguments) 4 { 5 var nameAttribute = methodInfo 6 .GetCustomAttributes(typeof(BindingNameAttribute), true) 7 .FirstOrDefault() as BindingNameAttribute; 8 if (nameAttribute != null) 9 { 10 return nameAttribute.Name; 11 } 12 return base.GetName(methodInfo, arguments); 13 } 14 }
工廠接口現在可以像下面這樣定義:
1 public interface IStatusCollectorFactory 2 { 3 [BindingName("TcpCollector")] 4 IStatusCollector GetTcpCollector(); 5 6 [BindingName("FileCollector")] 7 IStatusCollector GetFileCollector(); 8 }
工廠類型注冊應該變成下面這樣:
1 Kernel.Bind(x => x.FromThisAssembly() 2 .SelectAllInterfaces() 3 .EndingWith("Factory") 4 .BindToFactory(() => new NameAttributeInstanceProvider()));
IStatusCollector注冊應該改成下面這樣:
1 Kernel.Bind<IStatusCollector>().To<TcpStatusCollector>().Named("TcpCollector"); 2 Kernel.Bind<IStatusCollector>().To<FileStatusCollector>().Named("FileCollector");
或者下面這樣:
1 Kernel.Bind<IStatusCollector>().To<TcpStatusCollector>().NamedLikeFactoryMethod((IStatusCollectorFactory f) => f.GetTcpCollector()); 2 Kernel.Bind<IStatusCollector>().To<FileStatusCollector>().NamedLikeFactoryMethod((IStatusCollectorFactory f) => f.GetFileCollector());