Object Builder Application Block
文/黃忠成
2006/9/21
一、IoC 簡介
IoC的全名是『Inversion of Control』,字面上的意思是『控制反轉』,要了解這個名詞的真正含意,得從『控制』這個詞切入。一般來說,當設計師撰寫一個Console程式時,控制權是在該程式上,她決定著何時該印出訊息、何時又該接受使用者輸入、何時該進行資料處理,如程式1。
程式1
using System; using System.Collections.Generic; using System.Text;
namespace ConsoleApplication2 {
{
{
} } } |
從整個流程上看來,OS將控制權交給了此程式,接下來就看此程式何時將控制權交回,這是Console模式的標準處理流程。程式1演譯了『控制』這個字的意思,那麼『反轉』這個詞的涵意呢?這可以用一個Windows Application來演譯,如程式2。
程式2
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms;
namespace WindowsApplication10 {
{
{ InitializeComponent(); }
{
} } } |
與程式1不同,當程式2被執行後,控制權其實並不在此程式中,而是在底層的Windows Forms Framework上,當此程式執行後,控制權會在Application.Run函式呼叫後,由主程式轉移到Windows Forms Framework上,進入等待訊息的狀態,當使用者按下了Form上的按鈕後,底層的Windows Forms Framework會收到一個訊息,接著會依照訊息來呼叫button1_Click函式,此時控制權就由Windows Forms Framework轉移到了主程式。程式2充份演譯了『控制反轉』的意含,也就是將原本位於主程式中的控制權,反轉到了Windows Forms Framework上。
二、Dependency Injection
IoC的中心思想在於控制權的反轉,這個概念於現今的Framework中相當常見,.NET Framework中就有許多這樣的例子,問題是!既然這個概念已經實作於許多Framework中,那為何近年來IoC會於社群引起這麼多的討論?著名的IoC實作體如Avalon、Spring又達到了什麼目的呢?就筆者的認知,IoC是一個廣泛的概念,主要中心思想就在於控制權的反轉,Windows Forms Framework與Spring在IoC的大概念下,都可以算是IoC的實作體,兩者不同之處在於究竟反轉了那一部份的控制權,Windows Forms Framework將主程式的控制權反轉到了自身上,Spring則是將物件的建立、釋放、配置等控制權反轉到自身,雖然兩者都符合IoC的大概念,但設計初衷及欲達成的目的完全不同,因此用IoC來統稱兩者,就顯得有些籠統及模糊。設計大師Martin Fowler針對Spring這類型IoC實作體提出了一個新的名詞『Dependency Injection』,字面上的意思是『依賴注入』。對筆者而言,這個名詞比起IoC更能描述現今許多宣稱支援IoC的Framework內部的行為,在Martin Fowler的解釋中, Dependency Injection分成三種,一是Interface Injection(介面注射)、Constructor Injection(建構子注射)、Setter Injection(設值注射)。
2-1、Why we need Dependency Injection?
OK,花了許多篇幅在解釋IoC與Dependency Injection兩個概念,希望讀者們已經明白這兩個名詞的涵意,在切入Dependency Injection這個主題前,我們要先談談為何要使用Dependency Injection,及這樣做帶來了什麼好處,先從程式3的例子開始。
程式3
using System; using System.Collections.Generic; using System.Text;
namespace DISimple {
{
{
accept.Execute();
} }
{
{
input = _dataProcessor.ProcessData(input);
}
{ _dataProcessor = dataProcessor; } }
{
}
{
#region IDataProcessor Members
{
}
#endregion }
{ #region IDataProcessor Members
{
}
#endregion } } |
這是一個簡單且無用的例子,但卻可以告訴我們為何要使用Dependency Injection,在這個例子中,必須在建立InputAccept物件時傳入一個實作IDataProcessor介面的物件,這是Interface Base Programming概念的設計模式,這樣做的目的是為了降低InputAccept與實作體間的耦合關係,重用InputAccept的執行流程,以此來增加程式的延展性。那這個設計有何不當之處呢?沒有!問題不在InputAccept、IDataProcessor的設計,而在於使用的方式。
InputAccept accept = new InputAccept(new PromptDataProcessor()); |
使用InputAccept時,必須在建立物件時傳入一個實作IDataProcess介面的物件,此處直接建立一個PromptDataProcessor物件傳入,這使得主程式與PromptDataProcessor物件產生了關聯性,間接的摧毀使用IDataProcessor時所帶來的低耦合性,那要如何解決這個問題呢?讀過Design Patterns的讀者會提出以Builder、Factory等樣式解決這個問題,如下所示。
//Factory InputAccept accept = new InputAccept(DataProcessorFactory.Create()); //Builder InputAccept accept = new InputAccept(DataProcessorBulder.Build()); |
兩者的實際流程大致相同,DataProcessorFactory.Create函式會依據組態檔的設定來建立指定的IDataProcessor實作體,回傳後指定給InputAccept,DataProcessBuilder.Build函式所做的事也大致相同。這樣的設計是將原本位於主程式中IDataProcessor物件的建立動作,轉移到DataProcessorFactory、DataProcessorBuilder上,這也算是一種IoC觀念的實現,只是這種轉移同時也將主程式與IDataProcessor物件間的關聯,平移成主程式與DataProcessorFactory間的關聯,當需要建立的物件一多時,問題又將回到原點,程式中一定會充斥著AFactory、BFactory等Factory物件。徹底將關聯性降到最低的方法很簡單,就是設計Factory的Factory、或是Builder的Builder,如下所示。
//declare public class DataProcessorFactory:IFactory .......... //Builder public class DataProcessorBuilder:IBuilder ........... .................... //initialize //Factory GenericFactory.RegisterTypeFactory(typeof(IDataProcessor),typeof(DataProcessorFactory)); //Builder GenericFactory.RegisterTypeBuilder(typeof(IDataProcessor),typeof(DataProcessorBuilder)); ................ //Factory InputAccept accept = new InputAccept(GenericFactory.Create(typeof(IDataProcessor)); //Builder InputAccept accept = new InputAccept(GenericBuilder.Build(typeof(IDataProcessor)); |
這個例子中,利用了一個GenericFactory物件來建立InputAccept所需的IDataProcessor物件,當GenericFactory.Create函式被呼叫時,她會查詢所擁有的Factory物件對應表,這個對應表是以type of base class/type of factory成對的格式存放,程式必須在一啟動時準備好這個對應表,這可以透過組態檔或是程式碼來完成,GenericFactory.Create函式在找到所傳入的type of base class所對應的type of factory後,就建立該Factory的實體,然後呼叫該Factory物件的Create函式來建立IDataProcessor物件實體後回傳。另外,為了統一Factory的呼叫方式,GenericFactory要求所有註冊的Factory物件必須實作IFactory介面,此介面只有一個需要實作的函式:Create。方便讀者易於理解這個設計概念,圖1以流程圖呈現這個設計的。
圖1
那這樣的設計有何優勢?很明顯的,這個設計已經將主程式與DataProcessorFactory關聯切除,轉移成主程式與GenericFactory的關聯,由於只使用一個Factory:GenericFactory,所以不存在於AFactory、BFactory這類問題。這樣的設計概念確實降低了物件間的關聯性,但仍然不夠完善,因為有時物件的建構子會需要一個以上的參數,但GenericFactory卻未提供途徑來傳入這些參數(想像當InputAccept也是經由GenericFactory建立時),當然!我們可以運用object[]、params等途徑來傳入這些參數,只是這麼做的後果是,主程式會與實體物件的建構子產生關聯,也就是間接的與實體物件產生關聯。要切斷這層關聯,我們可以讓GenericFactory自動完成InputAccept與IDataProcessor實體物件間的關聯,也就是說在GenericFactory中,依據InputAccept的建構子宣告,取得參數型別,然後使用該參數型別(此例就是IDataProcessor)來呼叫GenericFactory.Create函式建立實體的物件,再將這個物件傳給InputAccept的建構子,這樣主程式就不會與InputAccept的建構子產生關聯,這就是Constructor Injection(建構子注入)的概念。以上的討論,我們可以理出幾個重點,一、Dependency Injection是用來降低主程式與物件間的關聯,二、Dependency Injection同時也能降低物件間的互聯性,三、Dependency Injection可以簡化物件的建立動作,進而讓物件更容易使用,試想!只要呼叫GenericFactory.Create(typeof(InputAccept))跟原先的設計,那個更容易使用?不過要擁有這些優點,我們得先擁有著一個完善的架構,這就是ObjectBuilder、Spring、Avalon等Framework出現的原因。
PS:這一小節進度超前許多,接下來將回歸Dependency Injection的三種模式,請注意!接下來幾小節的討論是依據三種模式的精神,所以例子以簡單易懂為主,不考慮本文所提及的完整架構。 |
2-2、Interface Injection
Interface Injection指的是將原本建構於物件間的依賴關係,轉移到一個介面上,程式4是一個簡單的例子。
程式4
using System; using System.Collections.Generic; using System.Text;
namespace ConsoleApplication2 {
{
{
accept.Inject(new accept.Execute();
} }
{
{ _dataProcessor = dataProcessor; }
{
input = _dataProcessor.ProcessData(input);
} }
{
}
{
#region IDataProcessor Members
{
}
#endregion }
{ #region IDataProcessor Members
{
}
#endregion } } |
InputAccept物件將一部份的動作轉移到另一個物件上,雖說如此,但InputAccept與該物件並未建立依賴關係,而是將依賴關係建立在一個介面:IDataProcessor上,經由一個函式傳入實體物件,我們將這種應用稱為Interface Injection。當然,如你所見,程式4的手法在實務應用上並未帶來太多的好處,原因是執行Interface Injection動作的仍然是主程式,這意味著與主程式與該物件間的依賴關係仍然存在,要將Interface Injection的概念發揮到極致的方式有兩個,一是使用組態檔,讓主程式由組態檔中讀入DummaryDataProcessor或是PromptDataProcessor,這樣一來,主程式便可以在不重新編譯的情況下,改變InputAccept物件的行為。二是使用Container(容器),Avalon是一個標準的範例。
程式5
public class InputAccept implements Serviceable { private IDataProcessor m_dataProcessor;
public void service(ServiceManager sm) throws ServiceException { m_dataProcessor = (IDataProcessor) sm.lookup("DataProcessor"); }
public void Execute() { ........ string input = m_dataProcessor.ProcessData(input); ........ } } |
在Avalon的模式中,ServiceManager扮演著一個容器,設計者可以透過程式或組態檔,將特定的物件,如DummyDataProcessor推到容器中,接下來InputAccept就只需要詢問容器來取得物件即可,在這種模式下,InputAccept不需再撰寫Inject函式,主程式也可以藉由ServiceManager,解開與DummyDataProcessor的依賴關係。使用Container時有一個特質,就是Injection動作是由Conatiner來自動完成的,這是Dependency Injection的重點之一。
PS:在正確的Interface Injection定義中,組裝InputAccept與IDataProcessor的是容器,在本例中,我並未使用容器,而是提取其行為。 |
2-3、Constructor Injection
Constructor Injection意指建構子注入,主要是利用建構子參數來注入依賴關係,建構子注入通常是與容器緊密相關的,容器允許設計者透過特定函式,將欲注入的物件事先放入容器中,當使用端要求一個支援建構子注入的物件時,容器中會依據目標物件的建構子參數,一一將已放入容器中的物件注入。程式6是一個簡單的容器類別,其支援Constructor Injection。
程式6
public {
{
{
_stores = new
} }
{
"target object has more then one constructor,container can't peek one for you.");
{
paramArray.Add(pi.Name, GetInstance(pi.ParameterType)); }
}
{
{
{
{
cArray.Add(paramArray[pi.Name]);
cArray.Add(null); }
}
}
}
{
Stores[t] = impl;
Stores.Add(t, impl); }
{
Stores.Add(t, null); } } |
Container類別提供了兩個函式,RegisterImplement有兩個重載函式,一接受一個Type物件及一個不具型物件,她會將傳入的Type及物件成對的放入Stores這個Collection中,另一個重載函式則只接受一個Type物件,呼叫這個函式代表呼叫端不預先建立該物件,交由GetInstance函式來建立。GetInstance函式負責建立物件,當要求的物件型別存在於Stores記錄中時,其會取得該型別的建構子,並依據建構子的參數,一一呼叫GetInstance函式來建立物件。程式7是使用這個Container的範例。
程式7
class {
{
accept.Execute();
} }
public {
{
input = _dataProcessor.ProcessData(input);
}
{ _dataProcessor = dataProcessor; } }
public {
}
public { #region IDataProcessor Members
{
}
#endregion }
public { #region IDataProcessor Members
{
}
#endregion } |
2-4、Setter Injection
Setter Injection意指設值注入,主要概念是透過屬性的途徑,將依賴物件注入目標物件中,與Constructor Injection模式一樣,這個模式同樣需要容器的支援,程式8是支援Setter Injection的Container程式列表。
程式8
public {
{
{
_stores = new
} }
{
{
{
{
pd.SetValue(target, GetInstance(pd.PropertyType)); }
}
}
}
{
Stores[t] = impl;
Stores.Add(t, impl); }
{
Stores.Add(t, null); } } |
程式碼與Constructor Injection模式大致相同,兩者差異之處僅在於Constructor Injection是使用建構子來注入,Setter Injection是使用屬性來注入,程式9是使用此Container的範例。
程式9
class {
{
accept.Execute();
} }
{
{
{
}
{ _dataProcessor = value; } }
{
input = _dataProcessor.ProcessData(input);
} } |
2-5、Service Locator
在Martain Fowler的文章中,Dependency Injection並不是唯一可以將物件依賴關係降低的方式,另一種Service Locator架構也可以達到同樣的效果,從架構角度來看,Service Locator是一個服務中心,設計者預先將Servcie物件推入Locator容器中,在這個容器內,Service是以Key/Value方式存在。欲使用該Service物件的物件,必須將依賴關係建立在Service Locator上,也就是說,不是透過建構子、屬性、或是方法來取得依賴物件,而是透過Service Locator來取得。
三、ObjectBuilder Application Block
ObjectBuilder一開始出現於Microsoft所提出的Composite UI Application Block,主司物件的建立及釋放工作,她實現了本文前面所提及的Dependency Injection概念,同時在架構上提供了高度的延展性。運用ObjectBuilder來建立物件,設計師可以透過程式或組態檔,對物件建立與釋放的流程進行細部的調整,例如改變物件建立時所呼叫的Constructor(建構子),調整傳入的參數,於物件建立後呼叫特定函式等等。鑑於ObjectBuilder的功能逐漸完整,加上社群對於Dependency Injection實作體的強烈需求,Microsoft正式將ObjectBuilder納入Enterprise Library 2006中,並修改Caching、Logger、Security、Data Access等Application Block的底層,令其於ObjectBuilder整合,以此增加這些Application Block的延展性。就官方文件的說明,ObjectBuilder Application Block提供以下的功能。
表1
|
|
|
|
|
|
|
|
對於多數讀者來說,這些官方說明相當的隱誨,本文嘗試由架構角度切入,討論ObjectBuidler的主要核心概念,再透過實作讓讀者們了解,該如何使用ObjectBuidler。
3-1、The Architecture of Object Builder
圖2
圖2是ObjectBuilder中四個主要核心物件的示意圖,BuidlerContext是一個概念型的環境物件,在這個物件中,包含著一組Strategys物件,一組Polices物件,一個Locator物件, ObjectBuidler採用Strategys Pipeline(策略流)概念,設計師必須透過Strategy串列來建立物件,而Strategy會透過Polices來尋找『型別/id』對應的Policy物件,使用她來協助建立指定的物件。此處有一個必須特別提出來討論的概念,Strategy在架構上是與型別無關的,每個BuidlerContext會擁有一群Strategys物件,我們透過這個Strategys物件來建立任何型別的物件,不管建立的物件型別為何,都會通過這個Strategys Pipeline。這意味著,當我們希望於建立A型別物件後呼叫函式A1,於建立B型別物件後呼叫函式 B1時,負責呼叫函式的Strategy物件會需要一個機制來判別該呼叫那個函式,那就是Policy物件,BuilderContext中擁有一個Polices物件,其中存放著與『型別/id』對應的Policy物件,如圖3所示。
圖3
值得一提的是,Policy是以Type/id方式,也就是『型別/id』方式來存放,這種做法不只可以讓不同型別擁有各自的Policy,也允許同型別但不同id擁有各自的Policy。ObjectBuilder中的最後一個元素是Locator,Locator物件在ObjectBuidler中扮演著前述的Service Locator角色,設計師可以用Key/Value的方式,將物件推入Locator中,稍後再以Key值來取出使用。
3-2、Strategys
ObjectBuilder內建了許多Strategy,這些Strategy可以大略分成四種類型,如圖4。
圖4
Pre-Creation Strategy
Pre-Creation意指物件被建立前的初始動作,參與此階段的Strategy有:TypeMappingStrategy、PropertyReflectionStrategy、ConstructorReflectionStrategy、MethodReflectionStrategy及SingletonStrategy,稍後我們會一一檢視她們。
Creation Strategy
Creation類型的Strategy主要工作在於建立物件,她會利用Pre-Creation Strategys所準備的參數來建立物件,ObjectBuilder就是運用Pre-Creation的ConstructorReflectionStrategy及CreationStrategy來完成Constructor Injection動作。
Initialization Strategy
當物件建立後,會進入初始化階段,這就是Initialization Strategy階段,在此階段中,PropertySetterStrategy會與PropertyReflectionStrategy合作,完成Setter Injection。而MethodExecutionStrategy則會與MethodReflectionStrategy合作,在物件建立後,呼叫特定的函式,也就是Method Injection(視使用方式,Interface Injection是以此種方式完成的)。
Post-Initialization Strategy
在物件建立並完成初始化動作後,就進入了Post-Initialization Strategy階段,在此階段中,BuilderAwareStrategy會探詢已建立的物件是否實作了IBuilderAware介面,是的話就呼叫IBuilderAware.OnBuildUp函式。
關於物件釋放
先前曾經提過,ObjectBuidler在建立物件時,會一一呼叫所有Strategy來建立物件,同樣的!當釋放物件時,ObjectBuilder也會進行同樣的動作,不過方向是相反的,在內建的Strategy中,只有BuilderAwareStrategy會參與物件釋放的動作,在物件釋放時,BuilderAwareStrategy會探詢欲釋放的物件是否實作了IBuidlerAware介面,是的話就呼叫IBuidlerAware.OnTearDown函式。
3-3、A Simple Application
再怎麼詳細的說明,少了一個實例就很難讓人理解,本節以一個簡單的ObjectBuidler應用實例開始,一步步帶領讀者進入ObjectBuilder的世界。
程式10
using System; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace SimpleApp {
{
{
obj.SayHello();
} }
{
{
} } } |
這是一個相當陽春的例子,在程式一開始時建立了一個Builder物件,她是ObjectBuilder所提供的Facade物件,其會預先建立一般常用的Strategy串列,並於BuilderUp函式被呼叫時,建立一個BuilderContext物件,並將Srategy串列及Polices串列指定給該BuilderContext,然後進行物件的建立工作。
How Object Creating
要了解前面的例子中,TestObject物件究竟是如何被建立起來的,首先必須深入Builder物件的建構動作。
private
public BuilderBase() { }
public { get { return policies; } }
public { get { return strategies; } }
public Builder(IBuilderConfigurator<BuilderStage> configurator) { Strategies.AddNew<TypeMappingStrategy>(BuilderStage.PreCreation); Strategies.AddNew<SingletonStrategy>(BuilderStage.PreCreation); Strategies.AddNew<ConstructorReflectionStrategy>(BuilderStage.PreCreation); Strategies.AddNew<PropertyReflectionStrategy>(BuilderStage.PreCreation); Strategies.AddNew<MethodReflectionStrategy>(BuilderStage.PreCreation); Strategies.AddNew<CreationStrategy>(BuilderStage.Creation); Strategies.AddNew<PropertySetterStrategy>(BuilderStage.Initialization); Strategies.AddNew<MethodExecutionStrategy>(BuilderStage.Initialization); Strategies.AddNew<BuilderAwareStrategy>(BuilderStage.PostInitialization);
Policies.SetDefault<ICreationPolicy>(new
if (configurator != null) configurator.ApplyConfiguration(this); } |
當Buidler物件被建立時,其建構子會將前面所提及的幾個Strategys加到Strategies這個StrategyList Collection物件中,待BuildUp函式被呼叫時指定給新建立的BuilderContext物件。
public TTypeToBuild BuildUp<TTypeToBuild>(IReadWriteLocator locator, string idToBuild, object existing, params { return (TTypeToBuild)BuildUp(locator, typeof(TTypeToBuild), idToBuild, existing, transientPolicies); }
public string idToBuild, object existing, params { .................... return DoBuildUp(locator, typeToBuild, idToBuild, existing, transientPolicies); ................... }
private PolicyList[] transientPolicies) { IBuilderStrategyChain chain = strategies.MakeStrategyChain(); .............. IBuilderContext context = MakeContext(chain, locator, transientPolicies); .......................... object result = chain.Head.BuildUp(context, typeToBuild, existing, idToBuild); ....................... }
private IReadWriteLocator locator, params { ............. return } |
當Builder的泛型函式BuildUp函式被呼叫後,其會呼叫非泛型的BuildUp函式,該函式會呼叫DoBuildUp函式,此處會透過strategies(先前於Builder建構子時初始化的StrategyList物件)來取得Strategys串列,並指定給稍後由MakeContext函式建立的BuilderContext,最後呼叫Strategy串列中第一個Strategy的BuildUp函式來進行物件的建立動作。在這一連串的動作中,我們可以釐清幾個容易令人混淆的設計,第一!我們是透過Strategy串列,也就是IBuidlerStrategyChain.Head.BuildUp來建立物件,這個Head屬性就是Strategy串列中的第一個Strategy。第二!BuilderContext的作用在於,於呼叫各個Strategy.BuildUp函式時,給予她們存取此次建立動作所使用的Strategys及Policies等物件的機會。
Policy物件的用途
現在,我們弄清楚了Strategy的用途,BuilderContext的真正涵意,但還有兩個元素尚未釐清,其中之一就是Policy物件,前面曾經稍微提過,Strategy是與型別無關的設計概念,因此為了針對不同型別做個別的處理,我們需要另一個與型別相關的設計,那就是Policy物件,要確認這點,必須重返Builder的建構子。
public Builder(IBuilderConfigurator<BuilderStage> configurator) { .................. Policies.SetDefault<ICreationPolicy>(new ................. } |
這裡呼叫了Policies的SetDefault函式,Policies是一個PolicyList物件,其提供了推入(Set、SetDefault)及取出(Get)函式,允許設計者針對所有『型別/id』及特定『型別/id』指定對應的IBuilderPolicy物件,那這有什麼用呢?這個問題可以由CreationStrategy類別中的以下這段程式碼來回答。
public { if (existing != null) BuildUpExistingObject(context, typeToBuild, existing, idToBuild); else existing = BuildUpNewObject(context, typeToBuild, existing, idToBuild); return }
[SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.SerializationFormatter)] private object existing, string idToBuild) { ICreationPolicy policy = context.Policies.Get<ICreationPolicy>(typeToBuild, idToBuild); ......................... InitializeObject(context, existing, idToBuild, policy); return existing; }
private { ................ ConstructorInfo constructor = policy.SelectConstructor(context, type, id); ................... object[] parms = policy.GetParameters(context, type, id, constructor); ............... method.Invoke(existing, parms); } |
如你所見,CreationStrategy於建立物件時,會由Policies中取出『型別/id』對應的ICreationPolicy物件,接著利用她來取得ConstructorInfo(建構子函式),再以GetParameters函式來取得建構子所需的參數,最後呼叫此建構子。這段程式碼告訴我們Policy的真正用途,就是用來協助Strategy於不同『型別/id』物件建立時,採取不同的動作,這也就是說,Strategy與Policy通常是成對出現的。
Locator
最後一個尚未釐清的關鍵元素是Locator,我們於呼叫Builder的BuildUp函式時,建立了一個Locator物件並傳入該函式,這是用來做什麼的呢?在ObjectBuilder中,Locator扮演兩種角色,第一個角色是提供一個物件容器供Strategy使用,這點可以透過以下程式了解。
public { public object existing, string idToBuild) { DependencyResolutionLocatorKey key = new typeToBuild, idToBuild); if (context.Locator != null && context.Locator.Contains(key, SearchMode.Local)) { TraceBuildUp(context, typeToBuild, idToBuild, ""); return context.Locator.Get(key); } return } } |
SingletonStrategy是一個用來維持某一個物件只能有一份實體存在,當此Strategy被喚起時,其會先至Locator尋找目前要求的物件是否已被建立,是的話就取出該物件並傳回。Locator同時也可以作為一個Service Locator,這點可以由以下程式碼來驗證。
locator.Add("Test",new ............. TestObject obj = locator.Get<TestObject>("Test"); |
當然,這種手法有一個問題,那就是TestObject物件是預先建立後放在Locator中,這並不是一個好的設計,後面的章節我們會提出將Service Locator與Dependency Injection整合的手法。
PS:ObjectBuidler的Locator離完善的Service Locator還有段距離。 |
四、Dependency Injection With ObjectBuilder
ObjectBuilder支援Dependency Injection中定義的三種Injection模式,本章將一一介紹如何運用ObjectBuilder來實現。
4-1、Constructor Injection
Constructor Injection的精神在於使用建構子來進行注入動作,本節延用InputAccept的例子,程式11是改採ObjectBuilder進行Constructor Injection的例子。
程式11
using System; using System.Collections.Generic; using System.Text; using System.Configuration; using Microsoft.Practices.ObjectBuilder; using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.ObjectBuilder; using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
namespace OB_ConstructorInjectionTest {
{
{
creationPolicy.AddParameter(new new context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null); }
{
context.InnerChain.Add(new UseValueParameter(context);
typeof(InputAccept), null, null); accept.Execute();
} }
{
: this(new { }
{ InnerLocator = locator; SetLocator(InnerLocator); StrategyChain = InnerChain; SetPolicies(InnerPolicies);
Locator.Add(typeof(ILifetimeContainer), lifetimeContainer); } }
{
{
input = _dataProcessor.ProcessData(input);
}
{ _dataProcessor = dataProcessor; } }
{
}
{
#region IDataProcessor Members
{
}
#endregion }
{ #region IDataProcessor Members
{
}
#endregion } } |
程式於一開始時,建立了一個MyBuilderContext物件,會自行建立BuilderContext物件而不使用Builder物件的目的很單純,就是為了釐清個別Strategy究竟做了那些事,這點在使用Builder物件時,會因為內建的Strategy都已加入,而顯得有些模糊。在MyBuilderContext物件建立後,此處將一個CreationStrategy加到Strategy串列中,CreationStrategy這個Strategy被歸類為Creation階段,是真正建立物件的Strategy,緊接著UseValueParameter函式會被呼叫,這個函式中建立了一個ConstructorPolicy物件,並呼叫其AddParameter函式,加入一個ValueParameter物件,這個ValueParameter物件就對應著InputAccept的建構子所需的參數,CreationStrategy於物件建立後,會透過BuilderContext的Policies來取得『型別/id』對應的ICreationPolicy物件(本例就是ConstructorPolicy物件),然後呼叫ICreationPolicy.SelectionConstructor函式,這個函式必須根據呼叫者已用ICreationPolicy.AddParameter所傳入的參數來選擇正確的建構子,然後再呼叫這個建構子並填入參數值來完成物件建立工作,圖5是這整個流程的示意圖。
圖5
圖中讀者可能會有所迷惑的是,FormatterServices.GetSafeUninitializedObject函式是何作用?這是.NET Framework中一個建立物件的途徑,與一般new或是Activator.CreateInstance方式不同,GetSafeUninitializedObject函式並不會觸發該物件的建構子,只是單純的將物件建立起來而已,因此CreationStrategy才必須於最後呼叫對應的建構子。
Understanding Parameter
程式11中使用了一個ValueParameter物件,要知道這個物件的作用,我們得先了解Parameter在ObjectBuilder中所代表的意義,在三種注入模式中,有一個共通的規則,就是需要有參數來注入,Constructor Injection是透過建構子參數注入,而Interface Injection則是透過函數參數注入,Setter Injection則是透過屬性注入,因此參數是這三種注入模式都會用到的觀念,所以ObjectBuilder定義了IParameter介面,並提供一組實作此介面的參數物件,於注入時期由這些參數物件來取得參數值,如圖6。
圖6
ValueParameter
這是一個最簡單的Paramter物件,建構子如下所示:
public ValueParameter(Type valueType, object value) |
她的GetValue函式僅是將建構子傳入的value物件傳回而已。
DependencyParamter
DependencyParameter是一個功能強大的Parameter物件,程式12是以DependencyParameter來取代ValueParameter完成Constructor Injection的例子。
程式12
static {
creationPolicy.AddParameter(new typeof(PromptDataProcessor),NotPresentBehavior.CreateNew,SearchMode.Local)); context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null);
context.Policies.Set<ICreationPolicy>(creationPolicy2, typeof(PromptDataProcessor),null); } |
讀者可以發現,DependencyParameter並未要求建構者傳入任何物件實體,而是要求建構者傳入注入時對應的參數型別、參數名稱、實體型別、NotPersentBehavoir及SearchMode等參數,下面的程式列表是DependencyParameter的建構子:
public DependencyParameter(Type parameterType, string name, Type createType, NotPresentBehavior notPresentBehavior, SearchMode searchMode) |
第一個參數是參數的型別,第二個參數是參數的名稱,當ConstructorPolicy於SelectConstructor函式時,會依據這兩個參數來選取適合的建構子,第三個參數是實體物件的型別,以本例來說,就是以PromptDataProcessor這個型別建立物件來傳入需要IDataProcessor型別的建構子、函式或屬性,第四個參數則影響了DependencyParameter的取值動作,預設情況下,DependencyParameter會先至Locator中取值,這個動作會受到第五個參數:SearchMode的影響(稍後會介紹這一部份),如果找不到的話,就會依據此參數值來做動作,NotPersentBehavior這個列舉的定義如下:
public { CreateNew, ReturnNull, Throw, } |
CreateNew代表著當DependencyParameter於Locator找不到需要的值時,呼叫BuilderContext.HeadOfChain.BuildUp函式來建立該物件,以此例來說即是如此,所建立物件的型別就是PromptDataProcessor。ReturnNull則是回傳一個Null值,Throw則是直接拋出一個例外。好了,了解了整體流程後,現在讓我們一一釐清這個流程中剩下的部份,第一!於Locator找尋需要的值是什麼意思,試想一種情況,當我們在做Dependency Injection時,是否有某些欲注入物件是可重用的,也就是該物件可以只建立一個,注入多個不同的物件,讓這些物件共用這個注入物件,這就是DependencyParameter會先至Locator中找尋已推入的注入物件的原因,請參考程式13的例子。
程式13
static { context.InnerLocator.Add(new new
creationPolicy.AddParameter(new null,typeof(PromptDataProcessor),NotPresentBehavior.CreateNew,SearchMode.Local)); context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null);
context.Policies.Set<ICreationPolicy>(creationPolicy2, typeof(PromptDataProcessor),null); } |
這個例子預先建立了一個PromptDataProcessor物件,並以DependencyResolutionLocatorKey封裝後推入Locator中,這樣一來,當DependencyParameter取值時,就會依據參數的『型別/id』至Locator找尋需要的值,此時就會得到我們所推入的PromptDataProcessor物件,而不是建立一個新的,另外!只要於AddParameter所傳入的DependencyParameter是以IDataProcessor為參數型別,並以null為id(名稱)的話,那麼永遠都會傳回我們所推入Locator的PromptDataProcessor物件。第二個要釐清的是SearchMode的涵意,在ObjectBuilder的架構上,Locator是可以有Parent/Child關係的,當DependencyParameter要找尋需要的物件時,如果SearchMode是Local的話,那麼這個搜尋動作只會搜尋該Locator自身,如果是Up的話,那麼在該Locator自身搜尋不到時,就會往Parent Locator搜尋。第三個要釐清的是第二個ConstructorPolicy的建立動作,還記得嗎?我們提過Policy是『型別/id』相關的,當DependencyParameter無法於Locator找到需要的物件而透過BuildUp來建立物件時,該『型別/id』同樣需要一個ICreationPolicy來對應,否則將會引發Missing Policy的例外,注意!DependencyParameter所使用的name參數必須與設定Set<ICreationPolicy>時所傳入的第三個參數相同。最後一個問題是,如果每個『型別/id』都要設定對應的ICreationPolicy,豈不累人,ObjectBuilder當然沒有這麼不人性化,我們可以呼叫Policies.SetDefault來為所有『型別/id』預設一個ICreationPolicy,如程式14所示。
程式14
static {
creationPolicy.AddParameter(new null,typeof(PromptDataProcessor),NotPresentBehavior.CreateNew,SearchMode.Local)); context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null);
context.Policies.SetDefault<ICreationPolicy>(new } |
CreationParameter
與DependencyParameter相同,CreationParameter也會透過BuildUp來建立物件,不同的是其不會先搜尋Locator,也無法作參數型別與實體型別對應,因此無法適用於InputAccept這種以介面為介質的注入方式,必須與TypeMappingStrategy(後述)合用才能解決,如程式15所示。
程式15
static {
creationPolicy.AddParameter(new context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null);
context.Policies.Set<ITypeMappingPolicy>(mappingPolicy, typeof(IDataProcessor), null);
context.Policies.SetDefault<ICreationPolicy>(new }
static {
context.InnerChain.Add(new context.InnerChain.Add(new UseCreationParameter(context);
typeof(InputAccept), null, null); accept.Execute();
} |
CloneParameter
CloneParameter的建構子接受一個IParameter參數,當其GetValue函式被呼叫時,會透過從建構子指定的Parameter物件來取值,如果取得的值是實作了ICloneable介面的物件時,其將呼叫Clone函式來拷貝該值,否則傳回原值,下面的程式片斷是CloneParametr的建構子宣告。
public CloneParameter(IParameter param) |
LookupParameter
LookupParameter的建構子接受一個object型別的參數,當GetValue函式被呼叫時,會經由Locator.Get函式,以建構子所傳入的參數為鍵值,取得位於Locator中的值,下面的程式片斷為LookupParameter的建構子宣告。
public LookupParameter(object key) |
程式16則是將InputAccept範例改為使用LookupParameter的版本。
程式16
static { context.InnerLocator.Add("dataProcessor", new
context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null); context.Policies.SetDefault<ICreationPolicy>(new } |
InjectionConstructor Attribute
使用Paramerer物件來進行Consturctor Injection時,設計者必須在建立物件前,預先準備這些Parameter物件,雖然動作不算繁鎖,但若全部物件的建立都要這麼做,未免有些沒有效率,為此!ObjectBuilder提供了另一種較為簡單的方法,就是利用InjectionConstructor這個Attribute,再搭配上ConstructorReflectionStrategy物件,自動的為設計者準備這些Parmeter物件,程式17是修改為InjectionConstructor模式的版本。
程式17
static {
context.InnerChain.Add(new }
.......... public {
{
input = _dataProcessor.ProcessData(input);
}
CreateType=typeof(PromptDataProcessor))]IDataProcessor dataProcessor) { _dataProcessor = dataProcessor; } } |
要使用InjectionConstructor Attribute,我們必須在CreationStrategy這個Strategy前加入一個ConstructorReflectionStrategy物件,她會於建立物件動作時,探詢欲建立物件型別所提供的所有建構子,選取已標上InjectionConstrucor Attribute的那個為指定建構子,接著ConstructorReflectionStrategy會探詢該建構子的所有參數,查看是否標上Dependency Attribute,是的話就以其設定建立DependencyParameter,否則建立一個新的DependencyParameter,她會單以型別參數來建立DependencyParameter,最後ConstructorReflectionStrategy會以這些資訊來建立對應的ConstructorPolicy物件,完成整個物件建立動作。
Understanding Dependency Attribute
ConstructorReflectionStrategy依賴兩個關鍵的Attribute,一個是用來標示指定建構子的InjectionConstructor Attribute,另一個則是用來標示參數該如何取得的Dependency Attribute,此Attribute有四個屬性,分別對應到DependencyParameter的四個屬性,如表2。
表2
DependencyAttribute | DependencyParameter | 說明 |
Name | Name | id(名稱) |
CreateType | CreateType | 欲建立物件的實體型別 |
NotPersentBehavior | NotPersentBehavior | 當欲建立物件無法由Locator取得時的行為模式。 |
SearchMode | SearchMode | 對Locator的搜尋法則。 |
使用Dependency Attribute與ConsturctorReflectionStrategy模式的優點是設計者不需花費時間一一建立Parameter物件,而缺點就是CreateType參數,由於ConstructorReflectionStrategy依賴著Dependency Attribute的CreateType參數來決定實際建立物件的型別,這使得設計者必須在標示Dependency Attribute時,一併指定這個參數,否則ConstructorReflectionStrategy將會以參數型別做為建立實際物件時的型別,而在本例中,我們無法建立一個IDataProcessor物件,這點降低了程式的可訂制性。那這要如何解決呢?簡單的方法是撰寫一個新的Dependency Attribute、或是使用TypeMappingStrategy,複雜的則是撰寫一個新的ConstructorReflectionStrategy,後面的章節我們會再重訪這個問題。
Injection with DependencyResolutionLocator
前面談到DependencyParameter時曾經提過,她會先至Locator中搜尋需要的參數值,那麼這也意味著,在使用ConstructorReflectionStrategy時,我們可以將參數值先行推入Locator中,這樣就可以避開指定CreateType了,如程式18所示。
程式18
static { context.InnerChain.Add(new context.InnerChain.Add(new
"dataProcessor"),new }
[InjectionConstructor] public InputAccept([Dependency(Name="dataProcessor")]IDataProcessor dataProcessor) |
當然,這仍然會有一個問題,那就是必須預先建立PromptDataProcessor物件,而非於InputAccept物件建立時期建立,這是在不撰寫自定Dependency Attribute或Strategy,亦或是使用TypeMappingStrategy情況下的簡易解法。
DefaultCreationPolicy and ConstructorPolicy
ObjectBuilder內建了兩個ICreationPolicy的實作體,一是前面所使用的ConsturoctPolicy,二是DefaultCreationPolicy,與ConstructorPolicy不同,DefaultCationPolicy永遠使用預設的建構子,如下所示。
public { if (constructor != null) return constructor;
List<Type> types = new foreach (IParameter parm in parameters) types.Add(parm.GetParameterType(context));
return type.GetConstructor(types.ToArray()); } |
而呼叫該建構子時所需的參數,則直接以BuildUp函式,依據參數的『型別/id』來建立,沒有與Parameter的互動。
public { ParameterInfo[] parms = constructor.GetParameters(); object[] parmsValueArray = new
for (int i = 0; i < parms.Length; ++i) parmsValueArray[i] = context.HeadOfChain.BuildUp(context, parms[i].ParameterType, null, id);
return parmsValueArray; } |
由此可見,DefaultCreationPolicy有兩個特色,一是其會選擇頂端的建構子,二是其一律以BuidUp函式依據參數型別來建立參數物件,不需要設計者介入。那在何種情況下選擇DefaultCreationPolicy呢?一般來說,使用ConstructorPolicy時,因為其會依據設計者所加入的Parameter物件來選擇建構子,如果設計者未準備這些,那麼ConstructorPolicy將因無法取得適合的建構子而引發例外,雖然這點可以經由搭配ConstructorReflectionStrategy來解決,但使用ConstructorReflectionStrategy時必須搭配Dependency Attribtue及InjectionConstructor Attribute,所以也是個負擔。使用DefaultCreationPolicy就沒有這些問題了,缺點則是無法指定實際建立的參數物件型別,所以DefautlCreationPolicy通常被設定成預設的ICreationPolicy,主要作用在於當我們所建立的物件是簡單的,只有一個建構子,且不需要特別指定參數實際型別時,就交由她來處理,而需要特別處理的,就運用『型別/id』對應的ConstructorPolicy或是Dependency Attribute、Injection Constructor Attrbute搭配ConstructorReflectionStrategy來處理。
4-2、Interface Injection
Interface Injection在ObjectBuidler中可以經由Method Injection來完成,指的是在物件建立後,呼叫所指定的函式來完成初始化動作,而負責這個工作的就是MethodExecutionStrategy,本節持續延應InputAccept來示範如何於ObjectBuidler中實現Interface Injection。
MethodExecutionStrategy
要實現Interface Injection,除了必須使用CreationStrategy來建立物件外,還要使用另一個Strategy:MethodExecutionStrategy,她會在物件建立完成後,執行指定的函式,程式19是使用MethodExecutionStrategy來實現Interface Injection的例子。
程式19
static { context.InnerChain.Add(new
IMethodCallInfo new
policy.Methods.Add("SetDataProcessor", callInfo); context.Policies.Set<IMethodPolicy>(policy, typeof(InputAccept), null); }
static {
UseDependencyAttribute(context); context.Policies.SetDefault<ICreationPolicy>(new
null, null); accept.Execute();
}
public {
{ _dataProcessor = dataProcessor; }
{
input = _dataProcessor.ProcessData(input);
} } |
此處使用ValueParameter來進行呼叫指定函式時的參數注入動作,在使用MethodExecutionStrategy時,設計者必須先行建立呼叫函式時所需的MethodCallInfo物件,這是一個實作IMethodInfo介面的物件,設計者必須於此物件中指定欲呼叫的函式、及傳入的Parameter物件,下面是MethodInfo的建構子宣告。
public MethodCallInfo(string methodName) public MethodCallInfo(string methodName, params public MethodCallInfo(string methodName, params public MethodCallInfo(string methodName, IEnumerable<IParameter> parameters) public MethodCallInfo(MethodInfo method) public MethodCallInfo(MethodInfo method, params public MethodCallInfo(MethodInfo method, IEnumerable<IParameter> parameters) |
MethodInfo擁有許多重載的建構子,大概分成兩大類:函式名稱及MethodInfo物件,每類會分成四個,分別是無參數、使用params傳入參數值、使用params傳入IParamete物件、傳入IEnumerable<IParameter>物件。在MethodInfo物件建立後,接著就要將這些物件傳入IMethodPolicy物件,並指定給context.Policies物件,這樣就完成了Interface Injection的準備動作,之後建立InputAccept物件後,SetDataProcess函式就會被呼叫,同時會傳入指定的PromptDataProcessor物件。
How MethodExecutionStrategy Working?
當MethodExecutionStrategy的BuildUp函式被呼叫時,會透過context.Policies來取得型別對應的IMethodPolicy物件,如下所示。
IMethodPolicy policy = context.Policies.Get<IMethodPolicy>(type, id); |
然後會透過IMethodPolicy物件來取得所有需要處理的IMethodCallInfo物件,並一一呼叫其SelectMethod函式來取得欲呼叫函式,如下所示。
MethodInfo methodInfo = methodCallInfo.SelectMethod(context, type, id); |
SelectMethod函式會依據當初建立此IMethodCallInfo物件時所指定的函式名稱、參數數量及型別來取得對應函式的MethodInfo物件。於取得MethodInfo物件後,緊接著就是透過IMethodCallInfo.GetParameters函式來取得呼叫此函式時需傳入的參數值,如下所示。
object[] parameters = methodCallInfo.GetParameters(context, type, id, methodInfo); |
最後呼叫MethodInfo.Invoke函式來呼叫該函式就完成整個動作了。
methodInfo.Invoke(obj, parameters); |
好了,這就是MethodExecutionStrategy的整個流程,現在我們要釐清幾個可能會令人困惑的問題,第一!當欲呼叫的函式是重載,有多個同名函式時,SelectMethod依據什麼來決定要呼叫那一個?答案是參數數量及型別。第二!當使用Parameter物件傳入MethodCallInfo物件的建構子時,GetParameters函式會透過Parameter.GetValue來取值,那麼當直接以object[]方式傳入MethodCallInfo的建構子時呢?答案是該建構子會逐個為傳入的object建立ValueParameter物件,如下所示。
public MethodCallInfo(string methodName, params :this(methodName, null, ObjectsToIParameters(parameters)) { }
private { List<IParameter> results = new if (parameters != null) foreach (object parameter in parameters) results.Add(new return results.ToArray(); } |
最後一個問題是,可以進行一個以上的函式呼叫嗎?答案是可以,建立對應的MethodCallInfo物件,並加到IMethodPolicy後即可,呼叫的順序則是依照MethodCallInfo加入IMethodPolicy的順序。。
Use DependencyParameter
與Constructor Injection相同,你也可以使用DependencyParameter來進行Interface Injection動作,如程式20。
程式20
static { context.InnerChain.Add(new context.InnerChain.Add(new
new
policy.Methods.Add("SetDataProcessor", callInfo); context.Policies.Set<IMethodPolicy>(policy, typeof(InputAccept), null); } |
use MethodReflectionStrategy
如同ConstructorReflectionStrategy的作用一樣,ObjectBuilder也提供了供Method Injection使用的MethodReflectionStrategy物件,要使用她,我們必須為欲進行Method Injection的函式標上InjectionMethod Attribute,如程式21所示。
程式21
static { context.InnerChain.Add(new context.InnerChain.Add(new
context.InnerLocator.Add(new "dataProcessor"), new }
public {
[Dependency(Name="dataProcessor"))]IDataProcessor dataProcessor) { _dataProcessor = dataProcessor; } ........... } |
本例使用DependencyResolutionLocatorKey模式進行注入動作,有了Constructor Injection部份的解說,相信讀者對這種模式已經了然於胸了。
Injection with Dependency Attribute and CreateType
同樣的,我們也可以在Dependency Attribute中指定CreateType來達到同樣的效果,如程式22所示。
程式22
static { context.InnerChain.Add(new context.InnerChain.Add(new context.InnerChain.Add(new }
public {
[Dependency(Name="dataProcessor", CreateType=typeof(PromptDataProcessor))]IDataProcessor dataProcessor) { _dataProcessor = dataProcessor; } ............. } |
4-3、Setter Injection
ObjectBuilder使用PropertySetterStrategy來進行Setter Injection,用法與前述的Interface Injection模式大致相同,如程式23所示。
程式23
static { context.InnerChain.Add(new
policy.Properties.Add("DataProcessor", new new context.Policies.Set<IPropertySetterPolicy>(policy, typeof(InputAccept), null); }
static {
UsePropertySetter(context); context.Policies.SetDefault<ICreationPolicy>(new
typeof(InputAccept), null, null); accept.Execute();
}
public {
{
{
}
{ _dataProcessor = value; } }
{
input = _dataProcessor.ProcessData(input);
} } |
設計者必須預先建立PropertySetterInfo物件,並為其指定欲設定的屬性名稱及參數,PropertySetterInfo是一個實作了IPropertySetterInfo介面的物件,其建構子宣告如下。
public PropertySetterInfo(string name, IParameter value) public PropertySetterInfo(PropertyInfo propInfo, IParameter value) |
有了MethodCallInfo的經驗,讀者們對這些建構子應該不會有任何疑惑,應該會抱怨其不像MethodCallInfo般提供那麼多的選擇吧(笑)。在ProeprtySetterInfo建立後,接著只要將其加到IPropertySetterPolicy物件中,並依『型別/id』指定給context.Policies即可完成Setter Injection。
How PropertySetterStrategy Work?
當PropertySetterStrategy的BuildUp函式被呼叫時,會透過context.Policies來取得型別對應的IPropertySetterPolicy物件,如下所示。
IPropertySetterPolicy policy = context.Policies.Get<IPropertySetterPolicy>(type, id); |
然後會透過IMethodPoliIPropertySetterPolicyy物件來取得所有需要處理的IPropertySetterInfo物件,並一一呼叫其SelectProperty函式來取得欲設定的屬性,如下所示。
PropertyInfo propInfo = propSetterInfo.SelectProperty(context, type, id); |
SelectProperty函式會依據當初建立此IPropertySetterInfo物件時所指定的屬性名稱、參數來取得對應屬性的PropertyInfo物件。於取得PropertyInfo物件後,緊接著就是透過IPropertySetterInfo.GetValue函式來取得設定此屬性時需傳入的值,如下所示。
object value = propSetterInfo.GetValue(context, type, id, propInfo); |
最後呼叫PropertyInfo.SetValue函式來設定屬性值就完成整個動作了。
propInfo.SetValue(obj, value, null); |
這就是整個Setter Injection的流程,這裡只有一個問題,我們可以設定一個以上的屬性嗎?答案是肯定的,只要建立對應數量的PropertySetterInfo物件即可。
use DependencyParameter
同樣的,使用DependencyParameter也可以達到同樣的效果,如程式24。
程式24
static { context.InnerChain.Add(new context.InnerChain.Add(new
policy.Properties.Add("DataProcessor", new
typeof(PromptDataProcessor),NotPresentBehavior.CreateNew,SearchMode.Local))); context.Policies.Set<IPropertySetterPolicy>(policy, typeof(InputAccept), null); } |
use PropertyReflectionStrategy
相對於ConsturctorReflectionStrategy及MethodReflectionStrategy,ObjectBuilder也提供了一個同類型的PropertyReflectionStrategy,我們可以搭配Dependency Attribute及DependencyResolutionLocatorKey物件來達到同樣效果,如程式25。
程式25
static { context.InnerChain.Add(new context.InnerChain.Add(new context.InnerChain.Add(new
"DataProcessor"), new }
public {
{
{
}
{ _dataProcessor = value; } } ......... } |
Injection with Dependency Attribute and CreateType
我們也可以使用Dependency Attribute及CreateType參數來進行Setter Injection,如程式26。
程式26
static { context.InnerChain.Add(new context.InnerChain.Add(new context.InnerChain.Add(new }
public {
{
{
}
{ _dataProcessor = value; } } ............... } |
五、Misc
5-1、SingletonStrategy
SingletonStrategy可於物件實體首次建立後,將實體保留在Context中的Locator內的ILifetimeContainer物件中,之後相同型態、id相同的物件建立動作,都是傳回這個物件,這是Singleton模式的實現,如程式27。
程式27
using System; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace OB_SingletonTest {
{
{
context.InnerChain.Add(new context.InnerChain.Add(new context.Policies.Set<ISingletonPolicy>(new context.Policies.SetDefault<ICreationPolicy>(new
typeof(TestObject), null, null);
typeof(TestObject), null, null);
} }
{
: this(new { }
{ InnerLocator = locator; SetLocator(InnerLocator); StrategyChain = InnerChain; SetPolicies(InnerPolicies);
Locator.Add(typeof(ILifetimeContainer), lifetimeContainer); } }
{ } } |
要將一個『型別/id』標示為Singleton,設計者必須於Strategy串列中加入SingletonStrategy物件,並建立一個SingletonPolicy物件,這是一個實作了ISingletonPolicy介面的類別,其建構子如下。
public SingletonPolicy(bool isSingleton); |
CreatationStrategy在建立物件後,會從context.Policies中取出『型別/id』對應的ISingletonPolicy物件,以其IsSingleton屬性來決定建立的物件是否為Singleton模式,是的話就將該物件實體填入ILifetimeContainer中,同時以DependencyResolutionLocatorKey包裝該物件實體,放入Locator中,如下所示。
private { if (context.Locator != null) { ILifetimeContainer lifetime = context.Locator.Get<ILifetimeContainer>( typeof(ILifetimeContainer), SearchMode.Local);
if (lifetime != null) { ISingletonPolicy singletonPolicy = context.Policies.Get<ISingletonPolicy>( typeToBuild, idToBuild);
if (singletonPolicy != null && singletonPolicy.IsSingleton) { context.Locator.Add(new typeToBuild, idToBuild), existing); lifetime.Add(existing); ...... } } } } |
以上流程是當該物件實體尚未建立時的流程,假如以BuildUp建立的物件已經存在於Locator中,那麼SingletonStrategy的BuildUp函式將直接傳回Locator中的物件實體。
public { DependencyResolutionLocatorKey key = new typeToBuild, idToBuild);
if (context.Locator != null && context.Locator.Contains(key, SearchMode.Local)) { TraceBuildUp(context, typeToBuild, idToBuild, ""); return context.Locator.Get(key); } return } |
PS:注意,SingletonStrategy在該物件已經存在於Locator中時,是直接回傳,並不會呼叫後面如MethodExecutionStrategy、PropertySetterStrategy等Strategy。 |
5-2、TypeMappingStrategy
前面的章節早已使用過TypeMappingStrategy這個物件了,她主要負責『型別/id』的對應,例如將IDataProcessor介面型別的建立,替換成PromptDataProcessor型別,如程式28所示。
程式28
using System; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace OB_TypeMappingTest {
{
{
context.InnerChain.Add(new context.InnerChain.Add(new
context.Policies.Set<ITypeMappingPolicy>(policy, typeof(ITestInterface), null); context.Policies.SetDefault<ICreationPolicy>(new
context, typeof(ITestInterface), null, null); obj1.SayHello();
} }
{
: this(new { }
{ InnerLocator = locator; SetLocator(InnerLocator); StrategyChain = InnerChain; SetPolicies(InnerPolicies);
Locator.Add(typeof(ILifetimeContainer), lifetimeContainer); } }
{
}
{
{
} } } |
TypeMappingStrategy必須搭配TypeMappingPolicy物件使用,TypeMappingPolicy是一個實作ITypeMappingPolicy介面的物件,建構子宣告如下。
public TypeMappingPolicy(Type type, string id) |
第一個參數是映射的實體型別,以本例來說就是TestObject,第二個參數是識別id,接著將其加入context.Policies中,如下所示。
context.Policies.Set<ITypeMappingPolicy>(policy, typeof(ITestInterface), null) |
當TypeMappingStrategy的BuildUp函式被呼叫時,她會以『型別/id』取得對應的ITypeMappingPolicy物件,透過她來取得對應的型別,之後將使用這個型別呼叫下一個Strategy的BuildUp函式,這就是Type Mapping的流程。
PS:注意,Type Mapping型別必須相容,如介面->實作、基礎類別->衍生類別。 |
5-3、BuildAwareStrategy
BuildAwareStrategy可以於實作IBuilderAware介面物件建立或釋放時,呼叫對應的OnBuildUp或OnTearDown函式,如程式29所示。
程式29
using System; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace OB_BuildAwareTest {
{
{
context.InnerChain.Add(new context.InnerChain.Add(new
context.Policies.SetDefault<ICreationPolicy>(new
typeof(TestObject), null, null); context.HeadOfChain.TearDown(context, obj);
} }
{
: this(new { }
{ InnerLocator = locator; SetLocator(InnerLocator); StrategyChain = InnerChain; SetPolicies(InnerPolicies);
Locator.Add(typeof(ILifetimeContainer), lifetimeContainer); } }
{ #region IBuilderAware Members
{
}
{
}
#endregion } } |
與其它的Strategy物件不同,BuilderAwareStrategy並不需要Policy物件的協助,她只是判斷建立的物件是否實作了IBuilderAware介面。
5-4、BuildUp的第三、四個參數
截至目前為止,我們的例子在呼叫BuildUp函式時,第三及四個參數都傳入null,這兩個參數的用途究竟為何呢?這要先從BuildUp函式的宣告談起。
object BuildUp(IBuilderContext context, Type typeToBuild, object existing, string idToBuild); |
當我們於呼叫BuildUp函式指定existing為一物件實體時,CreationStrategy將不會建立任何新的物件,只會進行Singleton模式物件的相關動作,然後就呼叫下一個Strategy物件的BuildUp函式,簡單的說!在CreationStrategy後的Strategy仍然會運行,例如Method Injection、Setter Injection都會再次運行,程式30可以協助讀者理解這兩個參數的用途。
程式30
using System; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace OB_ExistingTest {
{
{
context.InnerChain.Add(new
context.Policies.Set<ICreationPolicy>(policy, typeof(TestObject), null);
typeof(TestObject), null, null);
typeof(TestObject), obj, null);
} }
{
: this(new { }
{ InnerLocator = locator; SetLocator(InnerLocator); StrategyChain = InnerChain; SetPolicies(InnerPolicies);
Locator.Add(typeof(ILifetimeContainer), lifetimeContainer); } }
{
{
{
} }
{ _id = id; } } } |
BuildUp的第四個參數則主導著ObjectBuilder的型別識別及物件識別機制,請先看程式31的例子。
程式31
using System; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace OB_IDTesting {
{
{
context.InnerChain.Add(new context.InnerChain.Add(new
context.Policies.SetDefault<ICreationPolicy>(new
pp1.Properties.Add("ID", pi1); context.Policies.Set<IPropertySetterPolicy>(pp1, typeof(TestObject), "TO1");
pp2.Properties.Add("ID", pi2); context.Policies.Set<IPropertySetterPolicy>(pp2, typeof(TestObject), "TO2");
null, "TO1");
null, "TO2");
} }
{
: this(new { }
{ InnerLocator = locator; SetLocator(InnerLocator); StrategyChain = InnerChain; SetPolicies(InnerPolicies);
Locator.Add(typeof(ILifetimeContainer), lifetimeContainer); } }
{
{
{
}
{ _id = value; } } } } |
在這個例子中,我們建立了兩個PropertySetterPolicy物件,分別以ID2、ID2為id加到了context.Policies中,當CreationStrategy建立物件時,她是以下面的程式碼來取得對應的Policy物件。
private
{ ICreationPolicy policy = context.Policies.Get<ICreationPolicy>(typeToBuild, idToBuild); .................. |
這段程式碼告訴我們一個重點,ObjectBuidler是以『型別/id』來做型別識別動作,也就是說TestObject+"ID1"、TestObject+"ID2"被ObjectBuilder視為兩個不同的物件建立動作,你可以分別為其設定專屬的Policy物件,也可以於呼叫BuildUp函式時,指定不同的id來建立同型別,但不同id的物件。另一個會使用『型別/id』來做識別的是DependencyResolutionLocatorKey物件,我們之前常使用她來完成Injection動作,而SingletonStrategy、DependencyParameter也都是運用她來完成所需完成的工作,其建構子如下所示。
public DependencyResolutionLocatorKey(Type type, string id) |
這意味著,當我們使用SingletonStrategy時,可以利用『型別/id』來建立兩個同型別但不同id的Singleton物件,如程式32所示。
程式32
using System; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace OB_SingletonTwoTest {
{
{
context.InnerChain.Add(new context.InnerChain.Add(new context.Policies.Set<ISingletonPolicy>(new context.Policies.Set<ISingletonPolicy>(new context.Policies.SetDefault<ICreationPolicy>(new
null, "ID1");
null, "ID2");
} }
{
: this(new { }
{ InnerLocator = locator; SetLocator(InnerLocator); StrategyChain = InnerChain; SetPolicies(InnerPolicies);
Locator.Add(typeof(ILifetimeContainer), lifetimeContainer); } }
{ } } |
這個例子將TestObject+"ID1"、TestObject+"ID2"設定為兩個不同的Singleton物件,所以當首次建立並指定id時,所建立出來的兩個物件是相異的,也就是說,可以利用『型別/id』來建出兩個Singleton系統。
5-5、StrategyList
在本文一開始的範例中,我們使用Builder物件來建立物件,她使用了一個StrategyList物件來處理Strategy串列,這個物件提供了兩個重要的函式,一是MakeStrategyChain,她會將StrategyList中的Strategy輸出成BuilderStrategyChain物件,這是一個實作了IBuilderStrategyChain介面的物件,也是IBuilderContext所要求的Strategy串列物件。第二個函式是MakeReverseStrategyChain,她會將內含的Strategys反相排序後輸出成BuilderStrategyChain物件,這個動作是為了準備TearDown時所需的Strategy串列,還記得前面提過,TearDown的Strategy順序應該與建立時完全相反,這樣才能讓物件與其相關的子物件適當的釋放。
5-6、TStageEnum
StrategyList是一個泛型物件,她接受一個Enum型別,會依照Enum中所定義的元素來建立Strategy串列或是反相排序,要了解這個設計的原意,我們得先看看ObjectBuilder中所預定義,用來指定給StrategyList的列舉。
public { PreCreation, Creation, Initialization, PostInitialization } |
讀者可以查覺,這與我們先前將Strategy分成四種類型的方式相呼應,StrategyList會依據PreCreation、Creation、Initialization、PostInitialization的順序來產生BuilderStrategyChain物件,這樣就不會因為錯置Strategy的順序,導致程式不正常(例如,先加入CreationStrategy再加入TypeMappingStrategy時,TypeMappingStrategy將無法運作)。Builder物件充份展示了BuilderStage與StrategyList的運用方式。
public Builder(IBuilderConfigurator<BuilderStage> configurator) { Strategies.AddNew<TypeMappingStrategy>(BuilderStage.PreCreation); Strategies.AddNew<SingletonStrategy>(BuilderStage.PreCreation); Strategies.AddNew<ConstructorReflectionStrategy>(BuilderStage.PreCreation); Strategies.AddNew<PropertyReflectionStrategy>(BuilderStage.PreCreation); Strategies.AddNew<MethodReflectionStrategy>(BuilderStage.PreCreation); Strategies.AddNew<CreationStrategy>(BuilderStage.Creation); Strategies.AddNew<PropertySetterStrategy>(BuilderStage.Initialization); Strategies.AddNew<MethodExecutionStrategy>(BuilderStage.Initialization); Strategies.AddNew<BuilderAwareStrategy>(BuilderStage.PostInitialization); Policies.SetDefault<ICreationPolicy>(new if (configurator != null) configurator.ApplyConfiguration(this); } |
只要傳入的BuilderStage是正確的,不管TypeMappingStrategy是加在CreationStrategy前面或後面,皆可正常運作。不過同一類型的Strategy,但有順序需求的情況下,仍然要小心調整順序,程式32示範了運用BuilderStage所帶來的優點。
程式32
using System; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace OB_StrategyListTest {
{
{
builder.Policies.Set<ITypeMappingPolicy>(policy, typeof(ITestInterface), null);
} }
{
: this(null) { }
{ Strategies.AddNew<CreationStrategy>(BuilderStage.Creation); Strategies.AddNew<TypeMappingStrategy>(BuilderStage.PreCreation); Strategies.AddNew<SingletonStrategy>(BuilderStage.PreCreation); Strategies.AddNew<ConstructorReflectionStrategy>(BuilderStage.PreCreation); Strategies.AddNew<PropertyReflectionStrategy>(BuilderStage.PreCreation); Strategies.AddNew<MethodReflectionStrategy>(BuilderStage.PreCreation); Strategies.AddNew<PropertySetterStrategy>(BuilderStage.Initialization); Strategies.AddNew<MethodExecutionStrategy>(BuilderStage.Initialization); Strategies.AddNew<BuilderAwareStrategy>(BuilderStage.PostInitialization);
Policies.SetDefault<ICreationPolicy>(new
configurator.ApplyConfiguration(this); } }
{ }
{ } } |
5-6、PolicyList
BuilderContext所定義的Policies物件型別為PolicyList,PolicyList物件以Dictionary<BuilderPolicyKey, IBuilderPolicy>物件來儲存設計者所加入的Policy物件,其中用來作為鍵值的BuilderPolicyKey類別建構子如下。
public BuilderPolicyKey(Type policyType, Type typePolicyAppliesTo, string idPolicyAppliesTo) |
第一個參數為policyType,也就是ICrationPolicy、ITypeMappingPolicy等之類,第二個參數是對應的型別,第三個參數則是id。設計者可以呼叫PolicyList.Set函式來加入一個Policy至內部的儲存體中,該函式會依據傳入的參數建立BuilderPolicyKey做為鍵值,然後將Policy加到Dictionary中,如下所示。
public string idPolicyAppliesTo) { BuilderPolicyKey key = new typePolicyAppliesTo, idPolicyAppliesTo); lock (lockObject) { policies[key] = policy; } } |
另一個泛型類型的Set函式也可以達到同樣的效果。
public Type typePolicyAppliesTo, string idPolicyAppliesTo) where TPolicyInterface : IBuilderPolicy { Set(typeof(TPolicyInterface), policy, typePolicyAppliesTo, idPolicyAppliesTo); } |
設計者可以透過PolicyList.Get函式來取得對應的Policy物件,該函式如下所示。
public TPolicyInterface Get<TPolicyInterface>(Type typePolicyAppliesTo, string idPolicyAppliesTo) where TPolicyInterface : IBuilderPolicy { return (TPolicyInterface)Get(typeof(TPolicyInterface), typePolicyAppliesTo, idPolicyAppliesTo); }
public { BuilderPolicyKey key = new typePolicyAppliesTo, idPolicyAppliesTo); lock (lockObject) { IBuilderPolicy policy;
if (policies.TryGetValue(key, out policy)) return policy; BuilderPolicyKey defaultKey = new if (policies.TryGetValue(defaultKey, out policy)) return policy; return } } |
SetDefault則可以用一個Policy來提供給所有型別使用,Get函式在找不到對應『型別/id』對應的Policy時,就會以該Policy回傳。
六、Locator
ObjectBuilder利用Locator物件來實現Service Locator,也利用Locator來進行Dependency Injection,在ObjectBuilder的架構上,Locator有兩種類型,一是Readonly Locator,顧名思義,這類Locator只允許讀取、不允許新增。二是ReadWriteLocator,她是允許新增、讀取類的Locator。我們可以從Visual Studio 2005的Class Diagram來觀察ObjectBuilder中的Locator階層架構。
圖7
6-1、Readonly Locator
ObjectBuidler定義了一個IReadableLocator介面,所有的Locator都必須直接或間接實作此介面,內建實作此介面的類別是ReadableLocator,她是一個抽象類別。真正完成實作可用的是ReadOnlyLocator,這個Locator只允許讀取,不允許新增。
6-2、ReadWrite Locator
ObjectBuilder中支援讀與寫的Locator是ReadWriterLocator,與ReadOnlyLocator一樣,她也是一個抽象類別,真正完成實作的是Locator類別。附帶一提,雖然Locator定義了蠻清楚的階層,但是BuilderContext只支援實作IReadWriterLocator介面的Locator。
6-3、WeakRefDictionary and Locator
Locator類別是我們一直都在使用的Locator,她是一個繼承自ReadWriterLocator的類別,值得一提的是,她使用一個WeakRefDictionary來儲存設計者所放入Locator的物件,WeakRefDictionary內部對於每個元素都會以WeakReference封裝,這意味著,Locator中的元素並無法保證一直都存在,因為CLR會在記憶體拮据時,先行釋放WeakRefernce所封裝的物件,這點讀者必須謹記。
七、Extending ObjectBuilder
ObjectBuilder除了支援三種Dependency Injection模式、Service Locator之外,最大的魅力應該來自於具高度延展性的架構,設計者可以透過撰寫Strategy、Policy、Locator等類別來參與物件的建立動作,本章以兩個範例來證明這點,一是EventSetterStrategy,她提供Event Injection功能,二是PoolStrategy,提供Pool模式的物件建立。
7-1、EventSetterStrategy
ObjectBuidler提供了Constructor Injection、Interface Injection(Method Ijection)、Setter Injection(Property Injection)三種Injection模式,雖然ObjectBuilder只提供了Propety式的Setter Injection,不過我們可以藉助於ObjectBuilder高度的延展性架構,讓ObjectBuidler也能支援Event Injection。
IEventSetterInfo
Event Injection與Property Injection同屬Setter Injection模式,兩者運作的模式也極為相似,ObjectBuilder在Property Injection部份是由ProperySeterInfo、PropertySetterPolicy及PropertySetterStrategy三個類別所構築而成,我們可以依循這個既定架構,實作Event Injection功能。首要必須定義一個IEventSetterInfo介面,這相對於IPropertySetterInfo介面之於Property Injection。
程式33
public {
} |
IEventSetterInfo介面定義了兩個函式,SelectEvent函式是用來取得欲Injection事件的EventInfo物件,EventSetterStrategy會呼叫此函式來取得欲Injection事件的EventInfo物件,然後透過EventInfo.AddHandler來進行注入動作,這個注入動作所使用的值是透過呼叫IEventSetterInfo.GetValue函式來取得,此介面的實作程式碼如34。
程式34
public {
#region IEventSetterInfo Members
{
}
{
}
#endregion
{ _name = name; _value = value; } } |
IEventSetterPolicy
前面提過,Strategy是與型別無關的設計,因此需要Policy的協助,我們所設計的EventSetterStrategy也是一樣,Event Injection必須具備針對不同『型別/id』進行Event Injection的能力,所以必須設計一個IEventSetterPolicy介面,該介面必須直接或間接繼承自IBuilderPolicy介面,這是ObjectBuilder對於Policy的規範。
程式35
public {
} |
針對同一『型別/id』物件可能需要注入一個以上的事件,此介面定義了一個Dictionary<string,IEventSetterInfo>物件,讓設計者可以指定一個以上的Event Injection動作,36是此介面的實作。
程式36
public {
#region IEventPolicy Members
{
{
} }
#endregion } |
EventSetterStrategy
完成了基礎類別的設計與實作後,剩下的就是Strategy,也就是EventSetterStrategy的設計與實作了,設計上,EventSetterStrategy只有一個任務,就是於BuildUp函式被呼叫時,透過『型別/id』經由context.Locator取得對應的IEventSetterPolicy物件,再透過她取得欲進行注入動作的IEventSetterInfo物件,接著呼叫IEventSetterInfo.SelectEvent函式取得EventInfo物件,最後呼叫IEventSetterInfo.GetValue取得欲注入的Event Handler物件,然後呼叫EventInfo.AddHandler函式完成注入動作。
程式37
public {
object existing, string idToBuild) {
InjectEvents(context, existing, idToBuild);
}
{
{
{
TraceBuildUp(context, type, id, "Event Setter", eventInfo.Name);
eventSetterInfo.GetValue(context, type, id, eventInfo) as } } } } |
Testing
EventSetterStrategy的使用方式與PropertySetterStrategy相似,如38所示。
程式38
using System; using System.ComponentModel; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace EventSetterTest {
{
{
builder.Strategies.AddNew<EventSetterStrategy>(BuilderStage.Initialization);
policy.Events.Add("Call", new new builder.Policies.Set<IEventSetterPolicy>(policy, typeof(TestObject), null);
obj.RaiseCall();
}
{
} }
{
{
{ _events.AddHandler(_onCall, value); }
{ _events.RemoveHandler(_onCall, value); } }
{
handler(this, args); }
{ OnCall(EventArgs.Empty); } } } |
圖8是此程式的運行結果。
圖8
7-2、PoolStrategy
GoF的書中,提出了三種物件管理Pattern,一是Singleton,意味著物件一旦建立後,就存放於某個儲存體中,之後所有要求物件的建立動作,都將會獲得同樣的物件實體,在ObjectBuilder中實現這個Pattern的就是SingletonStrategy。第二個Pattern是SingleCall模式,意味所有的物件建立動作都會獲得一個新的物件實體,跟new、create等語言所定義的物件建立模式相同,在Service模式中,SingleCall也意味著Service物件會在要求到達時建立,結束後就立即的釋放,這兩個模式都可以用ObjectBuilder輕易的實現。第三種Pattern就是Pool,也就是說在特定儲存體中維持一定數量的物件實體,當要求物件建立動作時,系統會遍尋儲存體中的物件,如果有物件標示為未使用狀態,那麼系統就回傳該物件,並將該物件標示為使用中,本節將實作一個PoolStrategy,讓ObjectBuilder可以具備Pool的能力。
PoolFactory
Pool Pattern的核心就是一個可以於儲存體中管理物件的能力,此處使用筆者書中所設計的PoolFactory類別來完成這個目的。
程式39
using System; using System.Collections; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace Orphean.WinFormHelper.Framework.Factorys {
{
}
{
{
}
{ _max = max; _limit = limit; _storage = storage; }
{ _context = context; }
{
{
{
} }
}
{
{
{
_storage[0].GetType().Name)); }
{
{ p.InUse = true;
} }
p1.InUse = true; p1.obj = obj; _storage.Add(p1);
} }
{
p.InUse = false; }
#region IObjectFactory Members
{
}
{
{
((IDisposable)obj).Dispose();
_storage.Remove(p);
} PutObject(obj); }
#endregion
#region IDisposable Members
{
{
{
((IDisposable)p.obj).Dispose(); } } }
#endregion } } |
本文的重點在於ObjectBuilder的應用與延伸,所以此處就不在贅述PoolFactory的實作細節。
IPoolPolicy
PoolStrategy在架構上與SingletonStrategy類似,我們必須設計一個IPoolPolicy介面,該介面的定義如程式40。
程式40
public {
} |
此介面只定義了一個Pool屬性,用來告訴PoolStrategy那個『型別/id』是需要Pool,那個又是不需要的,雖然設計者可以針對要Pool的『型別/id』來指定IPoolPolicy,如果有特定物件不需要Pool動作,那就不指定IPoolPocy即可,但是我們無法排除一種情況,那就是系統裡大多數物件都需要Pool,僅有特定的物件不需要Pool,此時要特別對一個個物件設定IPoolPolicy的話,會相當的繁瑣。此時設計者可以以SetDefault來加入IPoolPolicy物件,將所有物件標示為可Pool,再針對不需要Pool的物件來指定IPoolPolicy。程式41是實作此介面的程式碼列表。
程式41
public {
#region IPoolPolicy Members
{
{
} }
#endregion
{ _isPool = isPool; } } |
PoolStrategy
PoolStrategy必須在BuildUp函式運用PoolFactory來取得要求的物件,在設計上,我們會為每個『型別/id』建立獨立的PoolFactory物件,這意味著每個『型別/id』的物件數量是獨立管理的。
程式42
using System; using System.Collections; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder; using Orphean.WinFormHelper.Framework.Factorys;
namespace OB_PoolStrategy {
{
new
object existing, string idToBuild) {
{
{
{ factory = context.Locator.Get<PoolObjectFactory>(key);
{ _poolObjectCreating = true;
{ existing = factory.AcquireObject(typeToBuild); }
{ _poolObjectCreating = false; } } }
{ factory = new _poolObjectCreating = true;
{ existing = factory.AcquireObject(typeToBuild); }
{ _poolObjectCreating = false; }
context.Locator.Add(key, factory); }
_factoryMap.Add(existing, factory); } }
}
{
{
factory.ReleaseObject(item); _factoryMap.Remove(item); }
} }
{
: this(null, null) { }
{
}
{
}
{
}
{
}
{
} } } |
在BuildUp函式被呼叫時,PoolStrategy會透過context.Policies取得『型別/id』對應的IPoolPolicy物件,判斷此次建立動作是否使用Pool,是的話就以『型別/id』至Locator中取出PoolFactory,如果Locator已經有該PoolFactory時,就直接呼叫PoolFactory.AcquireObject函式來取得物件實體,如果Locator中無對應的PoolFactory時,就建立一個並放入Locator中。在這個建立流程中有幾個重點,第一!我們將PoolFactory儲存在Locator中,因此需要一個類似DependencyResolutionLocatorKey的物件,用來做為由Locator取出PoolFactory的鍵值,這個物件必須覆載Equal、GetHashCode兩個函式,因為Locator會呼叫這兩個函式來比對鍵值,這個物件就是PoolLocatorKey。第二!PoolFactory在儲存體中沒有可使用物件時,會呼叫BuilderContext.HeadChain.BuildUp函式來建立該物件,這會引發重進入的問題,BuilderContext.HeadChain.BuildUp函式將會再次觸發PoolStrategy的BuildUp,而這裡又會再次呼叫BuilderContext.HeadChain.BuildUp,造成重入的問題,所以此處利用一個旗標:poolObjectCreating來解決這個問題。第三!PoolStrategy必須在TearDown函式被呼叫時,呼叫PoolFactory.ReleaseObject來將該物件歸還,此時會遭遇到一個問題,因為TearDown函式只會傳入物件實體,沒有id的資訊,這使得PoolStrategy無法於此處取得對應的PoolFactory物件,為了解決此問題,PoolStrategy宣告了一個_factoryMap物件,她是一個WeakRefDictionary<object, object>類別物件,在物件實體於BuildUp函式被建立後,PoolStrategy會將object/PoolFactory成對放入_factoryMap中,這樣就能於TearDown時以物件實體至_factoryMap中取出對應的PoolFactory物件了。
Testing
PoolStrategy的使用方式與SingletonStrategy類似,程式43是應用的程式碼列表。
程式43
using System; using System.Collections.Generic; using System.Text; using Microsoft.Practices.ObjectBuilder;
namespace OB_PoolStrategy {
{
{
builder.Strategies.AddNew<PoolStrategy>(BuilderStage.PreCreation);
builder.Policies.Set<IPoolPolicy>(policy, typeof(TestObject), null);
builder.TearDown<TestObject>(locator, obj1); builder.TearDown<TestObject>(locator, obj2);
} }
{ } } |
圖9是執行結果。
圖9
八、ObjectBuilder 實務
8-1、ObjectBuilder With Configuration
截至目前為止,本文一直以程式方式來組態ObjectBuidler建立物件所需的各種物件,但在實務上,這些動作應該是交由組態檔來負責,這樣才能在不重新編譯應用程式的情況下,改變其行為或增加其功能。很幸運的,Microsoft於ObjectBuilder的範例中提供了一個途徑來達到此目的,該範例定義了一個.xsd檔案,其內定義了Constructor Injection、Setter Injection、Singleton、Type Mapping所需要的schema,當然!這個xsd中也定義了Strategy的schema,允許設計者透過組態檔來添加物件建立時所需使用的Strategys。
Consturctor Injection With Configuration
我將Microsoft所提供可組態ObjectBuidler的範例中關於處理組態檔時的.xsd及相關檔案提取出來,並添加Method Injection時所需要的schema及程式碼,放置於本文的範例程式檔中,當讀者們需要使用組態檔這個功能時,可以將Config目錄中的ObjectBuilderXmlConfig.xsd、ObjectBuilderXmlConfig.xsx、ObjectBuilderXmlConfig.cs、ObjectBuilderXmlConfig.Generate.cs等檔案複製到專案目錄中,並將ObjectBuilderXmlConfig.xsd、ObjectBuilderXmlConfig.Generate.cs加入到專案中,完成後再將ObjectBuilderXmlConfig.xsd設定成Embedded Resource,如圖10所示。
圖10
接著將修改ObjectBuilderXmlConfig.cs中關於由Resource中取得.xsd內容的程式碼,修正namespace為專案的預設namespace即可。
程式44
private { XmlSerializer ser = new StringReader stringReader = new XmlSchema schema = XmlSchema.Read( Assembly.GetExecutingAssembly().GetManifestResourceStream( "OB_CSConfigurationTest.ObjectBuilderXmlConfig.xsd"), null); XmlReaderSettings settings = new settings.ValidationType = ValidationType.Schema; settings.Schemas.Add(schema); XmlReader reader = XmlReader.Create(stringReader, settings); ObjectBuilderXmlConfig configData = (ObjectBuilderXmlConfig) ser.Deserialize(reader); return configData; } |
要使用組態檔來完成Constructor Injection,我們必須在專案中新增一個xml檔案,內容如下所示。
<object-builder-config <build-rules> <build-rule type="OB_ConfigurationTest.InputAccept, OB_CSConfigurationTest" <constructor-params> <ref-param type="OB_ConfigurationTest.PromptDataProcessor, OB_CSConfigurationTest"/> </constructor-params> </build-rule> </build-rules> </object-builder-config> |
在ObjectBuilderXmlConfig.xsd定義中,build-rules代表著此BuilderContext中所有的物件建立規則,每個build-rule對應著一個『型別/id』,型別格式為<type,Assembly>,id部份則可透過添加name這個attribute來設定,未指定時就以null為預設值,如下。
<build-rule type="OB_ConfigurationTest.InputAccept, OB_CSConfigurationTest" name ="id1" mode="Instance"> |
每個build-rule可以擁有一個constructor-params區段,設計者可以在這個區段中添加value-param或是ref-param定義,前者是直接設定該參數的值,後者是透過reference方式來設值,本例中是將InputAccept建構子的第一個參數值指定為PromptDataProcessor。程式45是使用這個組態檔的程式列表。
程式45
using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Configuration; using Microsoft.Practices.ObjectBuilder; using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.ObjectBuilder; using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
namespace OB_ConfigurationTest {
{
{
{
{
accept.Execute();
} } } }
{
{
input = _dataProcessor.ProcessData(input);
}
{ _dataProcessor = dataProcessor; } }
{
}
{
#region IDataProcessor Members
{
}
#endregion }
{ #region IDataProcessor Members
{
}
#endregion } } |
Setter Injection With Configuration
同樣的,我們也可以透過組態檔來完成Setter Injection,此範例所使用的組態檔如下。
<object-builder-config <build-rules> <build-rule type="OB_CSPropertyInjectionTest.InputAccept, OB_CSPropertyInjectionTest" <property <ref-param
</property> </build-rule> </build-rules> </object-builder-config> |
設計者必須將要設定的屬性定義放置於build-rule區段中的property,property有一個name attribute,代表著欲設定屬性的名稱,如要設定一個以上的屬性,只需添加多個property區段即可,程式46是使用此組態檔的程式碼。
程式46
using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Configuration; using Microsoft.Practices.ObjectBuilder; using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.ObjectBuilder; using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
namespace OB_CSPropertyInjectionTest {
{
{
{
{
accept.Execute();
} } } }
{
[Dependency(Name="DataProcessor")]
{
{
}
{ _dataProcessor = value; } }
{
input = _dataProcessor.ProcessData(input);
} }
{
}
{
#region IDataProcessor Members
{
}
#endregion }
{ #region IDataProcessor Members
{
}
#endregion } } |
Method Injection With Configuration
Microsoft所提供的組態檔機制並未定義Method Injection的功能,我將其稍微修改來支援此功能,下面是Method Injection時所用的組態檔。
<object-builder-config <build-rules> <build-rule type="OB_CSMethodInjectionTest.InputAccept, OB_CSMethodInjectionTest" <method <ref-param type="OB_CSMethodInjectionTest.PromptDataProcessor, OB_CSMethodInjectionTest"/> </method> </build-rule> </build-rules> </object-builder-config> |
用法與property大致相同,程式47是程式碼列表。
程式47
using System; using System.Collections.Generic; using System.Text; using System.IO; using Microsoft.Practices.ObjectBuilder;
namespace OB_CSMethodInjectionTest {
{
{
{
{
accept.Execute();
} } } }
{
{ _dataProcessor = dataProcessor; }
{
input = _dataProcessor.ProcessData(input);
} }
{
}
{
#region IDataProcessor Members
{
}
#endregion }
{ #region IDataProcessor Members
{
}
#endregion } } |
Singleton With Configuration
透過組態檔,也可以定義那個『型別/id』的物件是使用Singleton模式,如下所示。
<build-rule |
Type Mapping With Configuration
我們也可以在組態檔中定義Type Mapping,如下所示。
<build-rule type="OB_CSMethodInjectionTest.IDataProcessor, OB_CSMethodInjectionTest" <mapped-type type="OB_CSMethodInjectionTest.PromptDataProcessor, OB_CSMethodInjectionTest"/> </build-rule> |
Customize Strategys
透過組態檔,也可以定義建立物件時所需要的Strategys,如下所示。
<object-builder-config <strategies <strategy type="Microsoft.Practices.ObjectBuilder.TypeMappingStrategy, Microsoft.Practices.ObjectBuilder"/> <strategy type="Microsoft.Practices.ObjectBuilder.CreationStrategy, Microsoft.Practices.ObjectBuilder"/> <strategy type="Microsoft.Practices.ObjectBuilder.MethodExecutionStrategy, Microsoft.Practices.ObjectBuilder"/> ....................... </object-builder-config> |
include-default屬性決定是否包含Builder物件所內建的Strategys。
九、後記
ObjectBuilder是一個相當不錯的Dependency Injection實作體,雖然相對於Spring、Avalon,ObjectBuilder並不是相當的完整,例如沒有完整的組態檔驅動特色,但她所具備的高度延展性,可以讓我們輕易的達到這個需求,且日後相信ObjectBuilder也會內建這些功能的。
1 comment:
This is great info to know.
Post a Comment