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