你知道Unity IoC Container是如何創建對象的嗎?
Unity是微軟P&P推出的一個開源的IoC框架,最新的官方版本是2.0。Unity之前的版本建立在一個稱為ObjectBuild的組件上,熟悉EnterLib的讀者,相信對ObjectBuild不會感到陌生。對於EnterLib 5.0之前的版本,ObjectBuild可以說是所有Application Block的基石。ObjectBuild提供一種擴展、可定製的對象創建方式,雖然微軟官方沒有將ObjectBuild和IoC聯係在一起,其本質可以看成是一個IoC框架。在Unity 2.0中,微軟直接將ObjectBuild(實際上是ObjectBuild的第二個版本ObjectBuild2)的絕大部分功能實現在了Unity中,而EnterLib則直接建立在Unity之上。由此可見Unity在EnterLib以及微軟其他一些開源框架(比如Software Factory)中的重要地位。
之前園子裏也有一些介紹EnterLib的文章,其中也不乏對Unity/ObjectBuild的介紹。雖然微軟官方聲稱Unity是一個輕量級的IoC框架,但是並不意味著Unity會很簡單。也正是因為Unity/ObjectBuild的複雜性,很多人撰文介紹Unity/ObjectBuild的時候,往往為了麵麵俱到,導致很多讀者不知所雲。最終的結果是,了解Unity/ObjectBuild的讀者能夠看懂,不懂的人讀了還是不懂。在本篇文章中,我試著換一種介紹方式:抓住Unity/ObjectBuild最本質的東西,剔除一些細枝末節,希望以一種全新的視角讓讀者了解Unity的本質。
一、從管道+上下文(Pipeline+Context)模式說起
如果要說Unity Container采用的怎樣的設計/架構模式的話,我的回答是“管道+上下文(Pipeline + Context)模式”(我不知道是否真的具有這樣一種叫法)。在《WCF技術剖析(卷1)》中介紹Binding一章中,我曾經對該模式作了一個類比:“比如有一個為居民提供飲用水的自來水廠,它的任務就是抽取自然水源,進行必要的淨化處理,最終輸送到居民區。淨化處理的流程可能是這樣的:天然水源被汲取到一個蓄水池中先進行雜質的過濾(我們稱這個池為過濾池);被過濾後的水流到第二個池子中進行消毒處理(我們稱這個池為消毒池);被消毒處理的水流到第三個池子中進行水質軟化處理(我們稱這個池為軟化池);最終水通過自來水管道流到居民的家中。”
當我們需要創建一個基礎架構對某種元素(例子中需要進行處理的水)進行一係列處理的時候,我們就可以將相應的處理邏輯(例子中的過濾、消毒和軟化)實現在相應“節點”(例子中的過濾池、消毒池和軟化池 )中。根據需要(比如水質情況)對相應的節點進行有序組合(水質的不同決定了處理工序的差異)從而構成一個管道(自來水廠整個水處理管道)。由於每一個節點具有標準的接口,我們可以對組成管道的各個節點具有任意重組,也可以為某種需要自定義節點,從而使我們的“管道”變得能夠適應所有的處理需要。
對於這樣的設計,其實我們並不陌生。比如ASP.NET的運行時就可以看成是一個由若幹HttpModule組成的處理HTTP請求的管道,WCF中Binding就是一個由若幹信道(Channel)組成的處理Message的管道。相同的設計還體現在.NET Remoting, BizTalk等相關框架和產品的設計上。
基於相應標準的“節點”進行有序組合構成管道,但是各個相對獨立的節點如何進行相應的協作呢?這就需要在整個管道範圍內共享一些上下文(Context),上下文是對管道處理對象和處理環境的封裝。ASP.NET運行時管道的上下文對象是HttpContext,而Binding管道的上下文是BindingContext。
二、UnityContainer是BuildStrategy的管道
作為一個IoC框架,Unity Container的最終目的動態地解析和注入依賴,最終提供(創建新對象或者提供現有對象)一個符合你要求的對象。為了讓整個對象提供處理流程變得可擴展和可訂製,整個處理過程被設計成一個管道。管道的每一個節點被稱為BuilderStrategy,它們按照各自的策略參與到整個對象提供處理流程之中。
除了對象的提供功能之外,Unity Container還提供另一個相反的功能:對象的回收。我們暫且將兩者稱之為Build-Up和Tear-Down。Build-Up和Tear-Down采用相同的處理機製。
左圖反映的就是Unity Container由若幹BuilderStrategy組成的一個用於進行對象的Build-Up和Tear-Down的管道。每一個BuildStrategy具有相同的接口(這個接口是IBuilderStrategy),它們具有四個標準的方法:PreBuildUp、PostBuildUp、PreTearDown和PostTearDown。從名稱我們不難看出,四個方法分別用於完成對象創建前/後和對象回收前後相應的操作。具體來說,當需要利用該管道創建某個對象的時候,先按照BuilderStrategy在管道中的順序調用PreBuildUp方法,到管道底端後,按照相反的順序調用PostBuildUp方法。
對於組成Unity Container管道的各個BuilderStrategy來說,它們彼此是相互獨立的,一個BuilderStrategy隻需要完成基於自身策略相應的操作,不需要知道其他BuilderStrategy的存在。隻有這樣才能實現對管道的靈活定製,真正實現可擴展。但是在真正工作的時候,彼此之間需要共享一些上下文以促進相互協作。在這裏,BuilderContext起到了這樣的作用。在Unity中,BuilderContext實現了IBuilderContext,我們不妨來看看IBuilderContext定義了些什麼:
1: public interface IBuilderContext
2: {
3: //Others...
4: bool BuildComplete { get; set; }
5: NamedTypeBuildKey BuildKey { get; set; }
6: NamedTypeBuildKey OriginalBuildKey { get; }
7: object CurrentOperation { get; set; }
8: object Existing { get; set; }
9: }
上麵對IBuilderContext的定義中,簡單起見,我刻意省略了一些屬性。在上述的屬性列表中,BuildComplete表示Build操作是否被標識為結束,如果某個BuilderStrategy已經完成了Build的操作,可以將其設置為True,這樣後續的BuilderStrategy就可以根據該值進行相應的操作(大部分將不作進行後續的Build);BuildKey和OriginalBuildKey是一個以Type + Name為組合的代表當前Build操作(通過CurrentOperation表示)的鍵;Existing屬性表示已經生成的對象,一般來講BuilderStrategy會將自己生成的對象賦給該值;而Strategies則代表了整個BuilderStrategy管道。
三、創建一個最簡單的BuilderStrategy
現在我們編寫一個最簡單不過的例子,看看UnityContainer是如何借助於BuilderStrategy管道進行對象的提供的(你可以通過這裏下載源代碼)。我們先來創建一個最簡單不過的BuilderStrategy,我們的策略就是通過反射的方式來創建對象,為此我們將該BuilderStrategy命名為ReflectionBuilderStrategy。
1: public class ReflectionBuilderStrategy:BuilderStrategy
2: {
3: public override void PreBuildUp(IBuilderContext context)
4: {
5: if (context.BuildComplete || null != context.Existing)
6: {
7: return;
8: }
9: var value = Activator.CreateInstance(context.BuildKey.Type);
10: if (null != value)
11: {
12: context.Existing = value;
13: context.BuildComplete = true;
14: }
15: }
16: }
ReflectionBuilderStrategy繼承自統一的基類BuilderStrategy。由於我們隻需要ReflectionBuildStrategy進行對象的創建,這裏我們隻需要重寫PreBuildUp方法。在PreBuildUp方法中,如果需要提供的對象已經存在(通過BuilderContext的Existing屬性判斷)或者Build操作已經完成(通過BuilderContext的BuildComplete屬性判斷),則直接返回。否則通過BuilderContext的BuildKey屬性得到需要創建對象的類型,通過反射的機製創建該對象,將其賦給BuilderContext的Existing屬性,並將BuildComplete設置成True。
現在BuilderStrategy已經創建成功,如何將它添加到UnityContainer的BuilderStrategy管道呢?一般地,我們需要為BuilderStrategy創建相應的擴展對象。為此,下麵我們創建了一個繼承自UnityContainerExtension的類型ReflectionContainerExtension:
1: public class ReflectionContainerExtension : UnityContainerExtension
2: {
3: protected override void Initialize()
4: {
5: this.Context.Strategies.AddNew<ReflectionBuilderStrategy>(UnityBuildStage.PreCreation);
6: }
7: }
在ReflectionContainerExtension中,我僅僅重寫了Initialize方法。在該方法中,通過Context屬性得到相應UnityContainer的BuilderStrategy管道,並調用AddNew方法將我們創建的ReflectionBuilderStrategy添加進取。泛型方法AddNew接受一個UnityBuildStage類型的枚舉。UnityBuildStage代表整個Build過程的某個階段,在這裏決定了添加的BuilderStrategy在管道中的位置。現在我們假設需要通過UnityContainer來創建下麵一個類型為Foo的對象:
1: public class Foo
2: {
3: public Guid Id { get; private set; }
4:
5: public Foo()
6: {
7: Id = Guid.NewGuid();
8: }
9: }
真正通過UnityContainer進行對象的提供實現在下麵的代碼中。由於UnityContainer在初始化的過程中會通過UnityDefaultStrategiesExtension這麼一個擴展,所以我特意通過調用RemoveAllExtension將其清除。然後調用AddExtension將我們上麵創建的ReflectionContainerExtension添加到UnityContainer的擴展列表中。最後3次調用UnityContainer的Resolve方法得到Foo對象,並將ID輸出。
1: static void Main(string[] args)
2: {
3: IUnityContainer container = new UnityContainer();
4: container.RemoveAllExtensions();
5: container.AddExtension(new ReflectionContainerExtension());
6: Console.WriteLine(container.Resolve<Foo>().Id);
7: Console.WriteLine(container.Resolve<Foo>().Id);
8: Console.WriteLine(container.Resolve<Foo>().Id);
9: }
10: }
下麵是輸出結果:
b38aa0b4-cc69-4d16-9f8c-8ea7baf1d853
ef0cefc2-ffac-4488-ad96-907fb568360b
08c538df-e208-4ef9-abe0-df7841d7ab60
四、通過自定義BuilderStrategy實現單例模式
上麵的例子已經能夠基本上反映出UnityContainer借助於BuilderStrategy管道的對象提供機製了。為了更加進一步的說明“管道”的存在,我們再自定義另一個簡單的BuilderStrategy,實現我們熟悉的單例模式(基於UnityContainer對象來說是單例)。下麵是是實現單例模式的BuilderStrategy:SingletonBuilderStrategy,和相應的Unity擴展。在SingletonBuilderStrategy中,我們通過一個靜態字典用於緩存創建成功的對象,該對象在字典中的Key為創建對象的類型。被創建的對象在PostCreate方法中被緩存,在PreCreate中被返回。為了將該SingletonBuilderStrategy至於管道的前端,在添加的時候指定的UnityBuildStage為Setup。
1: public class SingletonBuilderStrategy : BuilderStrategy
2: {
3: private static IDictionary<Type, object> cachedObjects = new Dictionary<Type, object>();
4:
5: public override void PreBuildUp(IBuilderContext context)
6: {
7: if (cachedObjects.ContainsKey(context.OriginalBuildKey.Type))
8: {
9: context.Existing = cachedObjects[context.BuildKey.Type];
10: context.BuildComplete = true;
11: }
12: }
13:
14: public override void PostBuildUp(IBuilderContext context)
15: {
16: if (cachedObjects.ContainsKey(context.OriginalBuildKey.Type) || null == context.Existing)
17: {
18: return;
19: }
20:
21: cachedObjects[context.OriginalBuildKey.Type] = context.Existing;
22: }
23: }
24:
25: public class SingletonContainerExtension : UnityContainerExtension
26: {
27: protected override void Initialize()
28: {
29: this.Context.Strategies.AddNew<SingletonBuilderStrategy>(UnityBuildStage.Setup);
30: }
31: }
現在,我們將基於SingletonBuilderStrategy的擴展添加到之前的程序中。再次運行我們的程序,你會發現輸出的ID都是一樣的,由此可見三次創建的對象均是同一個。
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: IUnityContainer container = new UnityContainer();
6: container.RemoveAllExtensions();
7: container.AddExtension(new ReflectionContainerExtension());
8: container.AddExtension(new SingletonContainerExtension());
9: Console.WriteLine(container.Resolve<Foo>().Id);
10: Console.WriteLine(container.Resolve<Foo>().Id);
11: Console.WriteLine(container.Resolve<Foo>().Id);
12: }
13: }
輸出結果:
254e1636-56e7-42f7-ad03-493864824d42
254e1636-56e7-42f7-ad03-493864824d42
254e1636-56e7-42f7-ad03-493864824d42
總結:雖然Unity具體的實現機製相對複雜,但是其本質就是本文所介紹的基於BuilderStrategy+BuilderContext的Pipeline+Context的機製。當你在研究Unity的具體實現原理的時候,抓住這個原則會讓你不至於迷失方向。
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-27 14:34:21