IoC主要體現了這樣一種設計思想:通過將一組通用流程的控制從應用轉移到框架之中以實現對流程的複用,同時採用“好萊塢原則”是應用程式以被動的方式實現對流程的定製。我們可以採用若干設計模式以不同的方式實現IoC,比如我們在上面介紹的模板方法、工廠方法和抽象工廠,接下來我們介紹一種更為有價值的IoC模式,即依賴注入(DI:Dependency Injection,以下簡稱DI)。
目錄
一、由外部容器提供服務物件
二、三種依賴注入方式
構造器注入
屬性注入
方法注入
三、例項演示:建立一個簡易版的DI框架
一、由外部容器提供服務物件
和上面介紹的工廠方法和抽象工廠模式一樣,DI旨在實現針對服務物件的動態提供。具體來說,服務的消費者利用一個獨立的容器(Container)來獲取所需的服務物件,容器自身在提供服務物件的過程中會自動完成依賴的解析與注入。話句話說,由DI容器提供的這個服務物件是一個” 開箱即用”的物件,這個物件自身直接或者間接依賴的物件已經在初始化的工程中被自動注入其中了。
舉個簡單的例子,我們建立一個名為Cat的DI容器類,那麼我們可以通過呼叫具有如下定義的擴充套件方法GetService從某個Cat物件獲取指定型別的服務物件。我之所以將其命名為Cat,源於我們大家都非常熟悉的一個卡通形象“機器貓(哆啦A夢)”。它的那個四次元口袋就是一個理想的DI容器,大熊只需要告訴哆啦A夢相應的需求,它就能從這個口袋中得到相應的法寶。DI容器亦是如此,服務消費者只需要告訴容器所需服務的型別(一般是一個服務介面或者抽象服務類),就能得到與之匹配的服務物件。
1 2 3 4 5 6 7 8 |
public class Foo { public IBar Bar{get; private set;} public Foo(IBar bar) { this.Bar = bar; } } |
對於我們在上一篇演示的MVC框架,我們在前面分別採用不同的設計模式對框架的核心型別MvcEngine進行了改造,現在我們採用DI的方式並利用上述的這個Cat容器按照如下的方式對其進行重新實現,我們會發現MvcEngine變得異常簡潔而清晰。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class MvcEngine { public Cat Cat { get; private set; } public MvcEngine(Cat cat) { this.Cat = cat; } public void Start(Uri address) { while (true) { Request request = this.Cat.GetService<Listener>().Listen(address); Task.Run(() => { Controller controller = this.Cat.GetService<ControllerActivator>().ActivateController(request); View view = this.Cat.GetService<ControllerExecutor>().ExecuteController(controller); this.Cat.GetService<ViewRenderer>().RenderView(view); }); } } } |
DI體現了一種最為直接的服務消費方式,消費者只需要告訴生產者(DI容器)關於所需服務的抽象描述,後者根據預先註冊的規則提供一個匹配的服務物件。這裡所謂的服務描述主要體現為服務介面或者抽象服務類的型別,當然也可以是包含實現程式碼的具體型別。至於應用程式對由框架控制的流程的定製,則可以通過對DI容器的定製來完成。如果具體的應用程式需要採用上面定義的SingletonControllerActivator以單例的模式來啟用目標Controller,那麼它可以在啟動MvcEngine之前按照如下的形式將SingletonControllerActivator註冊到後者使用的DI容器上。
1 2 3 4 5 6 7 8 9 10 |
public class App { static void Main(string[] args) { Cat cat = new Cat().Register<ControllerActivator, SingletonControllerActivator>(); MvcEngine engine = new MvcEngine(cat); Uri address = new Uri("http://localhost/mvcapp"); Engine.Start(address); } } |
二、三種依賴注入方式
一項確定的任務往往需要多個物件相互協作共同完成,或者某個物件在完成某項任務的時候需要直接或者間接地依賴其他的物件來完成某些必要的步驟,所以執行時物件之間的依賴關係是由目標任務來決定的,是“恆定不變的”,自然也無所謂“解耦”的說法。但是執行時的物件通過設計時的類來定義,類與類之間耦合則可以通過依賴進行抽象的方式來解除。
從服務使用的角度來講,我們藉助於一個服務介面對消費的服務進行抽象,那麼服務消費程式針對具體服務型別的依賴可以轉移到對服務介面的依賴上。但是在執行時提供給消費者總是一個針對某個具體服務型別的物件。不僅如此,要完成定義在服務介面的操作,這個物件可能需要其他相關物件的參與,換句話說提供的這個服務物件可能具有針對其他物件的依賴。作為服務物件提供者的DI容器,在它向消費者提供服務物件之前會自動將這些依賴的物件注入到該物件之中,這就是DI命名的由來。
如右圖所示,服務消費程式呼叫GetService()方法向DI容器索取一個實現了IFoo介面的某個型別的物件,DI容器會根據預先註冊的型別匹配關係建立一個型別為Foo的物件。此外,Foo物件依賴Bar和Baz物件的參與才能實現定義在服務介面IFoo之中的操作,所以Foo具有了針對Bar和Baz的直接依賴。至於Baz,它又依賴Qux,那麼後者成為了Foo的間接依賴。對於DI容器最終提供的Foo物件,它所直接或者間接依賴的物件Bar、Baz和Qux都會預先被初始化並自動注入到該物件之中。
從程式設計的角度來講,型別中的欄位或者屬性是依賴的一種主要體現形式,如果型別A中具有一個B型別的欄位或者屬性,那麼A就對B產生了依賴。所謂依賴注入,我們可以簡單地理解為一種針對依賴欄位或者屬性的自動化初始化方式。具體來說,我們可以通過三種主要的方式達到這個目的,這就是接下來著重介紹的三種依賴注入方式。
構造器注入
構造器注入就在在建構函式中藉助引數將依賴的物件注入到建立的物件之中。如下面的程式碼片段所示,Foo針對Bar的依賴體現在只讀屬性Bar上,針對該屬性的初始化實現在建構函式中,具體的屬性值由建構函式的傳入的引數提供。當DI容器通過呼叫建構函式建立一個Foo物件之前,需要根據當前註冊的型別匹配關係以及其他相關的注入資訊建立並初始化引數物件。
1 2 3 4 5 6 7 8 |
public class Foo { public IBar Bar{get; private set;} public Foo(IBar bar) { this.Bar = bar; } } |
除此之外,構造器注入還體現在對建構函式的選擇上面。如下面的程式碼片段所示,Foo類上面定義了兩個建構函式,DI容器在建立Foo物件之前首選需要選擇一個適合的建構函式。至於目標建構函式如何選擇,不同的DI容器可能有不同的策略,比如可以選擇引數做多或者最少的,或者可以按照如下所示的方式在目標建構函式上標註一個相關的特性(我們在第一個建構函式上標註了一個InjectionAttribute特性)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Foo { public IBar Bar{get; private set;} public IBaz Baz {get; private set;} [Injection] public Foo(IBar bar) { this.Bar = bar; } public Foo(IBar bar, IBaz):this(bar) { this.Baz = baz; } } |
屬性注入
如果依賴直接體現為類的某個屬性,並且該屬性不是隻讀的,我們可以讓DI容器在物件建立之後自動對其進行賦值進而達到依賴自動注入的目的。一般來說,我們在定義這種型別的時候,需要顯式將這樣的屬性標識為需要自動注入的依賴屬性,以區別於該型別的其他普通的屬性。如下面的程式碼片段所示,Foo類中定義了兩個可讀寫的公共屬性Bar和Baz,我們通過標註InjectionAttribute特性的方式將屬性Baz設定為自動注入的依賴屬性。對於由DI容器提供的Foo物件,它的Baz屬性將會自動被初始化。
1 2 3 4 5 6 7 |
public class Foo { public IBar Bar{get; set;} [Injection] public IBaz Baz {get; set;} } |
方法注入
體現依賴關係的欄位或者屬性可以通過方法的形式初始化。如下面的程式碼片段所示,Foo針對Bar的依賴體現在只讀屬性上,針對該屬性的初始化實現在Initialize方法中,具體的屬性值由建構函式的傳入的引數提供。我們同樣通過標註特性(InjectionAttribute)的方式將該方法標識為注入方法。DI容器在呼叫建構函式建立一個Foo物件之後,它會自動呼叫這個Initialize方法對只讀屬性Bar進行賦值。在呼叫該方法之前,DI容器會根據預先註冊的型別對映和其他相關的注入資訊初始化該方法的引數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public interface IFoo {} public interface IBar {} public interface IBaz {} public interface IQux {} public class Foo : IFoo { public IBar Bar { get; private set; } [Injection] public IBaz Baz { get; set; } public Foo() {} [Injection] public Foo(IBar bar) { this.Bar = bar; } } 22: public class Bar : IBar {} 23: 24: public class Baz : IBaz 25: { 26: public IQux Qux { get; private set; } 27: 28: [Injection] 29: public void Initialize(IQux qux) 30: { 31: this.Qux = qux; 32: } 33: } 34: 35: public class Qux : IQux {} |
我們在一個控制檯應用中按照如上的形式定義了四個服務型別(Foo、Bar、Baz和Qux),它們分別實現了各自的服務介面(IFoo、IBar、IBaz和IQux)。定義在Foo中的屬性Bar和Baz,以及定義在Baz中的屬性Qux是三個需要自動注入的依賴屬性,我們採用的注入方式分別是構造器注入、屬性注入和方法注入。
我們在作為應用入口的Main方法中編寫了如下一段程式。如下面的程式碼片段所示,在建立了作為DI容器的Cat物件之後,我們呼叫它的Register()方法註冊了服務型別和對應介面之間的匹配關係。然後我們呼叫Cat物件的GetService()方法通過指定的服務介面型別IFoo得到對應的服務物件,為了確保相應的依賴屬性均按照我們希望的方式被成功注入,我們將它們顯式在控制檯上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Program { static void Main(string[] args) { Cat cat = new Cat(); cat.Register<IFoo, Foo>(); cat.Register<IBar, Bar>(); cat.Register<IBaz, Baz>(); cat.Register<IQux, Qux>(); IFoo service = cat.GetService<IFoo>(); Foo foo = (Foo)service; Baz baz = (Baz)foo.Baz; Console.WriteLine("cat.GetService<IFoo>(): {0}", service); Console.WriteLine("cat.GetService<IFoo>().Bar: {0}", foo.Bar); Console.WriteLine("cat.GetService<IFoo>().Baz: {0}", foo.Baz); Console.WriteLine("cat.GetService<IFoo>().Baz.Qux: {0}", baz.Qux); } } |
這段程式被成功執行之後會在控制檯上產生如下所示的輸出結果,這充分證明了作為DI容器的Cat物件不僅僅根據指定的服務介面IFoo建立了對應型別(Foo)的服務物件,而且直接依賴的兩個屬性(Bar和Baz)分別以構造器注入和屬性注入的方式被成功初始化,間接依賴的屬性(Baz的屬性Qux)也以方法注入的形式被成功初始化。
1 2 3 4 |
cat.GetService<IFoo>(): Foo cat.GetService<IFoo>().Bar: Bar cat.GetService<IFoo>().Baz: Baz cat.GetService<IFoo>().Baz.Qux: Qux |
在對Cat容器的用法有了基本瞭解之後,我們來正式討論它的總體設計和具體實現。我們首先來看看用來標識注入建構函式、注入屬性和注入方法的InjectionAttribute特性的定義,如下面的程式碼片段所示,InjectionAttribute僅僅是一個單純的標識特性,它的用途決定了應用該特性的目標元素的型別(建構函式、屬性和方法)。
1 2 3 4 5 |
[AttributeUsage( AttributeTargets.Constructor| AttributeTargets.Property| AttributeTargets.Method, AllowMultiple = = false)] public class InjectionAttribute: Attribute {} |
如下所示的是Cat類的完整定義。我們採用一個ConcurrentDictionary型別的欄位來存放服務介面和具體服務型別之間的對映關係,這樣的對映關係通過呼叫Register方法實現。針對服務型別(服務介面型別或者具體服務型別均可)的服務物件提供機制實現在GetService方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
public class Cat { private ConcurrentDictionary<Type, Type> typeMapping = new ConcurrentDictionary<Type, Type>(); public void Register(Type from, Type to) { typeMapping[from] = to; } public object GetService(Type serviceType) { Type type; if (!typeMapping.TryGetValue(serviceType, out type)) { type = serviceType; } if (type.IsInterface || type.IsAbstract) { return null; } ConstructorInfo constructor = this.GetConstructor(type); if (null == constructor) { return null; } object[] arguments = constructor.GetParameters().Select(p => this.GetService(p.ParameterType)).ToArray(); object service = constructor.Invoke(arguments); this.InitializeInjectedProperties(service); this.InvokeInjectedMethods(service); return service; } protected virtual ConstructorInfo GetConstructor(Type type) { ConstructorInfo[] constructors = type.GetConstructors(); return constructors.FirstOrDefault(c => c.GetCustomAttribute<InjectionAttribute>() != null) ?? constructors.FirstOrDefault(); } protected virtual void InitializeInjectedProperties(object service) { PropertyInfo[] properties = service.GetType().GetProperties() .Where(p => p.CanWrite && p.GetCustomAttribute<InjectionAttribute>() != null) .ToArray(); Array.ForEach(properties, p =>p.SetValue(service, this.GetService(p.PropertyType))); } protected virtual void InvokeInjectedMethods(object service) { MethodInfo[] methods = service.GetType().GetMethods() .Where(m => m.GetCustomAttribute<InjectionAttribute>() != null) .ToArray(); Array.ForEach(methods, m=> { object[] arguments = m.GetParameters().Select(p => this.GetService(p.ParameterType)).ToArray(); m.Invoke(service, arguments); }); } } |
如上面的程式碼片段所示,GetService方法利用GetConstructor方法返回的建構函式建立服務物件。GetConstructor方法體現了我們採用的注入建構函式的選擇策略:優先選擇標註有InjectionAttribute特性的建構函式,如果不存在則選擇第一個公有的建構函式。執行建構函式傳入的引數是遞迴地呼叫GetService方法根據引數型別獲得的。
服務物件被成功建立之後,我們分別呼叫InitializeInjectedProperties和InvokeInjectedMethods方法針對服務物件實施屬性注入和方法注入。對於前者(屬性注入),我們在以反射的方式得到所有標註了InjectionAttribute特性的依賴屬性並對它們進行賦值,具體的屬性值同樣是以遞迴的形式呼叫GetService方法針對屬性型別獲得。至於後者(方法注入),我們同樣以反射的方式得到所有標註有InjectionAttribute特性的注入方法後自動呼叫它們,傳入的引數值依然是遞迴地呼叫GetService方法針對引數型別的返回值。