Object Builder Application Block
文
/
黃忠成
2006/9/21
三、
ObjectBuilder Application Block
ObjectBuilde
r
一開始
出現於
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
l
允許要求一個抽象物件或介面,ObjectBuilder會依據程式或組態檔的設定,傳回一個實體物件。
|
l
回傳一個既存物件,或是每次回傳一個新的物件(多半用於Dependency、Singleton情況,稍後會有詳細說明)。
|
l
透過特定的Factory建立一個物件,這個Factory可以依據組態檔的設定來建立物件(CustomFactory,隸屬於Enterprise Common Library)。
|
l
當物件擁有一個以上的建構子時,依據已有的參數,自動選取相容的建構子來建立要求的物件。(Consturctor Injection)
|
l
允許物件於建立後,透過程式或組態檔來賦值至屬性,或是呼叫特定的函式。(Setter Injection、Interface Injection)
|
l
提供一組Attribute,讓設計師可以指定需要Injection的屬性,亦或是於物件建立後需要呼叫的函式,也就是使用Reflection來自動完成Injection動作。
|
l
提供IBuilerAware介面,實作此介面的物件,ObjectBuilder會於建立該物件後,呼叫OnBuildUp或是OnTearDown函式。
|
l
提供TearDown機制,按建立物件的流程,反向釋放物件。
|
對於多數讀者來說,這些官方說明相當的隱誨,本文嘗試由架構角度切入,討論
ObjectBuidler
的主要核心概念,再透過實作讓讀者們了解,該如何使用
ObjectBuidler
。
3-1
、
The Architecture of Object Builder
圖2
圖
2
是
ObjectBuilder
中四個主要核心物件的示意圖,
BuidlerContext
是一個概念型的環境物件,在這個物件中,包含著一組
Strategys
物件,一組
Polices
物件,一個
Locato
r
物件, 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 Injectio
n
。而MethodExecutionStrategy則會與MethodReflectionStrategy合作,在物件建立後,呼叫特定的函式,也就是Method Injection(視使用方式,Interface Injection是以此種方式完成的)。
Post-Initialization Strategy
在物件建立並完成初始化動作後,就進入了
Post-Initialization Strateg
y
階段,在此階段中,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
{
class Program
{
static void Main(string[] args)
{
Builder builder = new Builder();
TestObject obj = builder.BuildUp<TestObject>(new Locator(), null, null);
obj.SayHello();
Console.ReadLine();
}
}
public class TestObject
{
public void SayHello()
{
Console.WriteLine("TEST");
}
}
}
|
這是一個相當陽春的例子,在程式一開始時建立了一個
Builder
物件,她是
ObjectBuilder
所提供的
Facade
物件,其會預先建立一般常用的
Strategy
串列,並於
BuilderUp
函式被呼叫時,建立一個
BuilderContext
物件,並將
Srategy
串列及
Polices
串列指定給該
BuilderContext
,然後進行物件的建立工作。
How Object Creating
要了解前面的例子中,
TestObject
物件究竟是如何被建立起來的,首先必須深入
Builder
物件的建構動作。
private
StrategyList<TStageEnum> strategies = new StrategyList<TStageEnum>();
public
BuilderBase()
{
}
public
PolicyList Policies
{
get { return policies; }
}
public
StrategyList<TStageEnum> Strategies
{
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 DefaultCreationPolicy());
if (configurator != null)
configurator.ApplyConfiguration(this);
}
|
當
Buidler
物件被建立時,其建構子會將前面所提及的幾個
Strategys
加到
Strategies
這個
StrategyList Collection
物件中,待
BuildUp
函式被呼叫時指定給新建立的
BuilderContext
物件。
public
TTypeToBuild BuildUp<TTypeToBuild>(IReadWriteLocator locator,
string
idToBuild, object existing, params PolicyList[] transientPolicies)
{
return (TTypeToBuild)BuildUp(locator, typeof(TTypeToBuild), idToBuild, existing, transientPolicies);
}
public
virtual object BuildUp(IReadWriteLocator locator, Type typeToBuild,
string idToBuild, object existing, params PolicyList[] transientPolicies)
{
....................
return DoBuildUp(locator, typeToBuild, idToBuild, existing, transientPolicies);
...................
}
private
object DoBuildUp(IReadWriteLocator locator, Type typeToBuild, string idToBuild, object existing,
PolicyList[] transientPolicies)
{
IBuilderStrategyChain chain = strategies.MakeStrategyChain();
..............
IBuilderContext context = MakeContext(chain, locator, transientPolicies);
..........................
object result = chain.Head.BuildUp(context, typeToBuild, existing, idToBuild);
.......................
}
private
IBuilderContext MakeContext(IBuilderStrategyChain chain,
IReadWriteLocator locator, params PolicyList[] transientPolicies)
{
.............
return new BuilderContext(chain, locator, policies);
}
|
當
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 DefaultCreationPolicy());
.................
}
|
這裡呼叫了
Policies
的
SetDefault
函式,
Policies
是一個
PolicyList
物件,其提供了推入
(Set
、
SetDefault)
及取出
(Get)
函式,允許設計者針對所有『型別
/id
』及特定『型別
/id
』指定對應的
IBuilderPolicy
物件,那這有什麼用呢?這個問題可以由
CreationStrategy
類別中的以下這段程式碼來回答。
public
override object BuildUp(IBuilderContext context, Type typeToBuild, object existing, string idToBuild)
{
if (existing != null)
BuildUpExistingObject(context, typeToBuild, existing, idToBuild);
else
existing = BuildUpNewObject(context, typeToBuild, existing, idToBuild);
return base.BuildUp(context, typeToBuild, existing, idToBuild);
}
[SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.SerializationFormatter)]
private
object BuildUpNewObject(IBuilderContext context, Type typeToBuild,
object
existing, string idToBuild)
{
ICreationPolicy policy = context.Policies.Get<ICreationPolicy>(typeToBuild, idToBuild);
.........................
InitializeObject(context, existing, idToBuild, policy);
return existing;
}
private
void InitializeObject(IBuilderContext context, object existing, string id, ICreationPolicy policy)
{
................
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
最後一個尚未釐清的關鍵元素是
Locato
r
,我們於呼叫Builder的BuildUp函式時,建立了一個Locator物件並傳入該函式,這是用來做什麼的呢?在ObjectBuilder中,Locator扮演兩種角色,第一個角色是提供一個物件容器供Strategy使用,這點可以透過以下程式了解。
public
class SingletonStrategy : BuilderStrategy
{
public
override object BuildUp(IBuilderContext context, Type typeToBuild,
object
existing, string idToBuild)
{
DependencyResolutionLocatorKey key = new DependencyResolutionLocatorKey(
typeToBuild, idToBuild);
if (context.Locator != null && context.Locator.Contains(key, SearchMode.Local))
{
TraceBuildUp(context, typeToBuild, idToBuild, "");
return context.Locator.Get(key);
}
return base.BuildUp(context, typeToBuild, existing, idToBuild);
}
}
|
SingletonStrategy
是一個用來維持某一個物件只能有一份實體存在,當此Strategy被喚起時,其會先至Locator尋找目前要求的物件是否已被建立,是的話就取出該物件並傳回。Locator同時也可以作為一個Service Locator,這點可以由以下程式碼來驗證。
locator.Add("Test",new TestObject());
.............
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
{
class Program
{
static void UseValueParameter(MyBuilderContext context)
{
ConstructorPolicy creationPolicy = new ConstructorPolicy();
creationPolicy.AddParameter(new ValueParameter(typeof(IDataProcessor),
new
PromptDataProcessor()));
context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null);
}
static void Main(string[] args)
{
MyBuilderContext context = new MyBuilderContext(new Locator());
context.InnerChain.Add(new CreationStrategy());
UseValueParameter(context);
InputAccept accept = (InputAccept)context.HeadOfChain.BuildUp(context,
typeof
(InputAccept), null, null);
accept.Execute();
Console.Read();
}
}
internal class MyBuilderContext : BuilderContext
{
public IReadWriteLocator InnerLocator;
public BuilderStrategyChain InnerChain = new BuilderStrategyChain();
public PolicyList InnerPolicies = new PolicyList();
public LifetimeContainer lifetimeContainer = new LifetimeContainer();
public MyBuilderContext()
: this(new Locator())
{
}
public MyBuilderContext(IReadWriteLocator locator)
{
InnerLocator = locator;
SetLocator(InnerLocator);
StrategyChain = InnerChain;
SetPolicies(InnerPolicies);
if (!Locator.Contains(typeof(ILifetimeContainer)))
Locator.Add(typeof(ILifetimeContainer), lifetimeContainer);
}
}
public class InputAccept
{
private IDataProcessor _dataProcessor;
public void Execute()
{
Console.Write("Please Input some words:");
string input = Console.ReadLine();
input = _dataProcessor.ProcessData(input);
Console.WriteLine(input);
}
public InputAccept(IDataProcessor dataProcessor)
{
_dataProcessor = dataProcessor;
}
}
public interface IDataProcessor
{
string ProcessData(string input);
}
public class DummyDataProcessor : IDataProcessor
{
#region
IDataProcessor Members
public string ProcessData(string input)
{
return input;
}
#endregion
}
public class PromptDataProcessor : IDataProcessor
{
#region
IDataProcessor Members
public string ProcessData(string input)
{
return "your input is: " + input;
}
#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
void UseDependencyParameter(MyBuilderContext context)
{
ConstructorPolicy creationPolicy = new ConstructorPolicy();
creationPolicy.AddParameter(new DependencyParameter(typeof(IDataProcessor),null,
typeof
(PromptDataProcessor),NotPresentBehavior.CreateNew,SearchMode.Local));
context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null);
ConstructorPolicy creationPolicy2 = new ConstructorPolicy();
context.Policies.Set<ICreationPolicy>(creationPolicy2, typeof(PromptDataProcessor),null);
}
|
讀者可以發現,
DependencyParameter
並未要求建構者傳入任何物件實體,而是要求建構者傳入注入時對應的參數型別、參數名稱、實體型別、
NotPersentBehavoi
r
及SearchMode等參數,下面的程式列表是DependencyParameter的建構子:
public
DependencyParameter(Type parameterType, string name,
Type createType, NotPresentBehavior notPresentBehavior, SearchMode searchMode)
|
第一個參數是參數的型別,第二個參數是參數的名稱,當
ConstructorPolicy
於
SelectConstructor
函式時,會依據這兩個參數來選取適合的建構子,第三個參數是實體物件的型別,以本例來說,就是以
PromptDataProcessor
這個型別建立物件來傳入需要
IDataProcessor
型別的建構子、函式或屬性,第四個參數則影響了
DependencyParameter
的取值動作,預設情況下,
DependencyParameter
會先至
Locator
中取值,這個動作會受到第五個參數:
SearchMode
的影響
(
稍後會介紹這一部份
)
,如果找不到的話,就會依據此參數值來做動作,
NotPersentBehavior
這個列舉的定義如下:
public
enum NotPresentBehavior
{
CreateNew,
ReturnNull,
Throw,
}
|
CreateNew
代表著當
DependencyParameter
於
Locator
找不到需要的值時,呼叫
BuilderContext
.HeadOfChain.BuildUp
函式來建立該物件,以此例來說即是如此,所建立物件的型別就是PromptDataProcessor。ReturnNull則是回傳一個Null值,Throw則是直接拋出一個例外。好了,了解了整體流程後,現在讓我們一一釐清這個流程中剩下的部份,第一!於Locator找尋需要的值是什麼意思,試想一種情況,當我們在做Dependency Injection時,是否有某些欲注入物件是可重用的,也就是該物件可以只建立一個,注入多個不同的物件,讓這些物件共用這個注入物件,這就是DependencyParameter會先至Locator中找尋已推入的注入物件的原因,請參考程式13的例子。
程式13
static
void UseDependencyParameter(MyBuilderContext context)
{
context.InnerLocator.Add(new DependencyResolutionLocatorKey(typeof(IDataProcessor), null),
new
PromptDataProcessor());
ConstructorPolicy creationPolicy = new ConstructorPolicy();
creationPolicy.AddParameter(new DependencyParameter(typeof(IDataProcessor),
null
,typeof(PromptDataProcessor),NotPresentBehavior.CreateNew,SearchMode.Local));
context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null);
ConstructorPolicy creationPolicy2 = new ConstructorPolicy();
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
void UseDependencyParameter(MyBuilderContext context)
{
ConstructorPolicy creationPolicy = new ConstructorPolicy();
creationPolicy.AddParameter(new DependencyParameter(typeof(IDataProcessor),
null
,typeof(PromptDataProcessor),NotPresentBehavior.CreateNew,SearchMode.Local));
context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null);
context.Policies.SetDefault<ICreationPolicy>(new ConstructorPolicy());
}
|
CreationParameter
與
DependencyParameter
相同,
CreationParameter
也會透過
BuildUp
來建立物件,不同的是其不會先搜尋
Locato
r
,也無法作參數型別與實體型別對應,因此無法適用於InputAccept這種以介面為介質的注入方式,必須與TypeMappingStrategy(後述)合用才能解決,如程式15所示。
程式15
static
void UseCreationParameter(MyBuilderContext context)
{
ConstructorPolicy creationPolicy = new ConstructorPolicy();
creationPolicy.AddParameter(new CreationParameter(typeof(IDataProcessor)));
context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null);
TypeMappingPolicy mappingPolicy = new TypeMappingPolicy(typeof(PromptDataProcessor), null);
context.Policies.Set<ITypeMappingPolicy>(mappingPolicy, typeof(IDataProcessor), null);
context.Policies.SetDefault<ICreationPolicy>(new ConstructorPolicy());
}
static
void Main(string[] args)
{
MyBuilderContext context = new MyBuilderContext(new Locator());
context.InnerChain.Add(new TypeMappingStrategy());
context.InnerChain.Add(new CreationStrategy());
UseCreationParameter(context);
InputAccept accept = (InputAccept)context.HeadOfChain.BuildUp(context,
typeof
(InputAccept), null, null);
accept.Execute();
Console.Read();
}
|
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
void UseLookupParameter(MyBuilderContext context)
{
context.InnerLocator.Add("dataProcessor", new PromptDataProcessor());
ConstructorPolicy creationPolicy = new ConstructorPolicy();
creationPolicy.AddParameter(new LookupParameter("dataProcessor"));
context.Policies.Set<ICreationPolicy>(creationPolicy, typeof(InputAccept), null);
context.Policies.SetDefault<ICreationPolicy>(new ConstructorPolicy());
}
|
InjectionConstructor Attribute
使用
Paramerer
物件來進行
Consturctor Injectio
n
時,設計者必須在建立物件前,預先準備這些Parameter物件,雖然動作不算繁鎖,但若全部物件的建立都要這麼做,未免有些沒有效率,為此!ObjectBuilder提供了另一種較為簡單的方法,就是利用InjectionConstructor這個Attribute,再搭配上ConstructorReflectionStrategy物件,自動的為設計者準備這些Parmeter物件,程式17是修改為InjectionConstructor模式的版本。
程式17
static
void UseInjectionConstructorAttribute(MyBuilderContext context)
{
context.InnerChain.Add(new ConstructorReflectionStrategy());
context.InnerChain.Add(new CreationStrategy());
}
..........
public
class InputAccept
{
private IDataProcessor _dataProcessor;
public void Execute()
{
Console.Write("Please Input some words:");
string input = Console.ReadLine();
input = _dataProcessor.ProcessData(input);
Console.WriteLine(input);
}
[InjectionConstructor]
public InputAccept([Dependency(Name="dataProcessor",
CreateType=typeof(PromptDataProcessor))]IDataProcessor dataProcessor)
{
_dataProcessor = dataProcessor;
}
}
|
要使用
InjectionConstructor Attribute
,我們必須在
CreationStrategy
這個
Strategy
前加入一個
ConstructorReflectionStrategy
物件,她會於建立物件動作時,探詢欲建立物件型別所提供的所有建構子,選取已標上
InjectionConstrucor Attribute
的那個為指定建構子,接著
ConstructorReflectionStrategy
會探詢該建構子的所有參數,查看是否標上
Dependency Attribute
,是的話就以其設定建立
DependencyParameter
,否則建立一個新的
Dependenc
yParameter
,她會單以型別參數來建立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 Attribut
e
、或是使用TypeMappingStrategy
,複雜的則是撰寫一個新的
ConstructorReflectionStrategy
,後面的章節我們會再重訪這個問題。
Injection with DependencyResolutionLocator
前面談到
DependencyParameter
時曾經提過,她會先至
Locator
中搜尋需要的參數值,那麼這也意味著,在使用
ConstructorReflectionStrategy
時,我們可以將參數值先行推入
Locator
中,這樣就可以避開指定
CreateType
了,如程式
18
所示。
程式
18
static
void UseDependencyResolution(MyBuilderContext context)
{
context.InnerChain.Add(new ConstructorReflectionStrategy());
context.InnerChain.Add(new CreationStrategy());
context.InnerLocator.Add(new DependencyResolutionLocatorKey(typeof(IDataProcessor),
"dataProcessor"
),new PromptDataProcessor());
}
[InjectionConstructor]
public
InputAccept([Dependency(Name="dataProcessor")]IDataProcessor dataProcessor)
|
當然,這仍然會有一個問題,那就是必須預先建立
PromptDataProcessor
物件,而非於
InputAccept
物件建立時期建立,這是在不撰寫自定
Dependency Attribut
e
或Strategy,亦或是使用TypeMappingStrategy
情況下的簡易解法。
DefaultCreationPolicy and ConstructorPolicy
ObjectBuilder
內建了兩個
ICreationPolicy
的實作體,一是前面所使用的
ConsturoctPolicy
,二是
DefaultCreationPolicy
,與
ConstructorPolicy
不同,
DefaultCationPolicy
永遠使用預設的建構子,如下所示。
public
ConstructorInfo SelectConstructor(IBuilderContext context, Type type, string id)
{
if (constructor != null)
return constructor;
List<Type> types = new List<Type>();
foreach
(IParameter parm in parameters)
types.Add(parm.GetParameterType(context));
return type.GetConstructor(types.ToArray());
}
|
而呼叫該建構子時所需的參數,則直接以
BuildUp
函式,依據參數的『型別
/id
』來建立,沒有與
Parameter
的互動。
public
object[] GetParameters(IBuilderContext context, Type type, string id, ConstructorInfo constructor)
{
ParameterInfo[] parms = constructor.GetParameters();
object[] parmsValueArray = new object[parms.Length];
for (int i = 0; i < parms.Length; ++i)
parmsValueArray[i] = context.HeadOfChain.BuildUp(context, parms[i].ParameterType, null, id);
return parmsValueArray;
}
|
由此可見,
DefaultCreationPolicy
有兩個特色,一是其會選擇頂端的建構子,二是其一律以
BuidU
p
函式依據參數型別來建立參數物件,不需要設計者介入。那在何種情況下選擇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
來完成,指的是在物件建立後,呼叫所指定的函式來完成初始化動作,而負責這個工作的就是
MethodExecutionStrateg
y
,本節持續延應InputAccept來示範如何於ObjectBuidler中實現Interface Injection。
MethodExecutionStrategy
要實現
Interface Injection
,除了必須使用
CreationStrategy
來建立物件外,還要使用另一個
Strategy
:
MethodExecutionStrategy
,她會在物件建立完成後,執行指定的函式,程式
19
是使用
MethodExecutionStrategy
來實現
Interface Injection
的例子。
程式
19
static
void UseMethodInfo(MyBuilderContext context)
{
context.InnerChain.Add(new CreationStrategy());
context.InnerChain.Add(new MethodExecutionStrategy());
IMethodCallInfo
callInfo = new MethodCallInfo("SetDataProcessor",
new
ValueParameter(typeof(IDataProcessor), new PromptDataProcessor()));
IMethodPolicy policy = new MethodPolicy();
policy.Methods.Add("SetDataProcessor", callInfo);
context.Policies.Set<IMethodPolicy>(policy, typeof(InputAccept), null);
}
static
void Main(string[] args)
{
MyBuilderContext context = new MyBuilderContext(new Locator());
UseDependencyAttribute(context);
context.Policies.SetDefault<ICreationPolicy>(new DefaultCreationPolicy());
InputAccept accept = (InputAccept)context.HeadOfChain.BuildUp(context, typeof(InputAccept),
null
, null);
accept.Execute();
Console.Read();
}
public
class InputAccept
{
private IDataProcessor _dataProcessor;
public void SetDataProcessor(IDataProcessor dataProcessor)
{
_dataProcessor = dataProcessor;
}
public void Execute()
{
Console.Write("Please Input some words:");
string input = Console.ReadLine();
input = _dataProcessor.ProcessData(input);
Console.WriteLine(input);
}
}
|
此處使用
ValueParameter
來進行呼叫指定函式時的參數注入動作,在使用
MethodExecutionStrategy
時,設計者必須先行建立呼叫函式時所需的
MethodCallInfo
物件,這是一個實作
IMethodInfo
介面的物件,設計者必須於此物件中指定欲呼叫的函式、及傳入的
Parameter
物件,下面是
MethodInfo
的建構子宣告。
public
MethodCallInfo(string methodName)
public
MethodCallInfo(string methodName, params object[] parameters)
public
MethodCallInfo(string methodName, params IParameter[] parameters)
public
MethodCallInfo(string methodName, IEnumerable<IParameter> parameters)
public
MethodCallInfo(MethodInfo method)
public
MethodCallInfo(MethodInfo method, params IParameter[] parameters)
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 object[] parameters)
:this(methodName, null, ObjectsToIParameters(parameters))
{
}
private
static IEnumerable<IParameter> ObjectsToIParameters(object[] parameters)
{
List<IParameter> results = new List<IParameter>();
if (parameters != null)
foreach (object parameter in parameters)
results.Add(new ValueParameter(parameter.GetType(), parameter));
return results.ToArray();
}
|
最後一個問題是,可以進行一個以上的函式呼叫嗎?答案是可以,建立對應的
MethodCallInfo
物件,並加到
IMethodPolicy
後即可,呼叫的順序則是依照
MethodCallInfo
加入
IMethodPolicy
的順序。。
Use DependencyParameter
與
Constructor Injection
相同,你也可以使用
DependencyParameter
來進行
Interface Injection
動作,如程式
20
。
程式
20
static
void UseDependencyParameter(MyBuilderContext context)
{
context.InnerChain.Add(new CreationStrategy());
context.InnerChain.Add(new MethodExecutionStrategy());
MethodCallInfo callInfo = new MethodCallInfo("SetDataProcessor",
new
DependencyParameter(typeof(IDataProcessor), "dataProcessor", typeof(PromptDataProcessor), NotPresentBehavior.CreateNew, SearchMode.Local));
IMethodPolicy policy = new MethodPolicy();
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
void UseDependencyResolverLocator(MyBuilderContext context)
{
context.InnerChain.Add(new CreationStrategy());
context.InnerChain.Add(new MethodReflectionStrategy());
context.InnerChain.Add(new MethodExecutionStrategy());
context.InnerLocator.Add(new DependencyResolutionLocatorKey(typeof(IDataProcessor),
"dataProcessor"
), new PromptDataProcessor());
}
public
class InputAccept
{
private IDataProcessor _dataProcessor;
[InjectionMethod]
public void SetDataProcessor(
[Dependency(Name="dataProcessor"))]
IDataProcessor
dataProcessor)
{
_dataProcessor = dataProcessor;
}
...........
}
|
本例使用
DependencyResolutionLocatorKey
模式進行注入動作,有了
Constructor Injection
部份的解說,相信讀者對這種模式已經了然於胸了。
Injection with Dependency Attribute and CreateType
同樣的,我們也可以在
Dependency Attribute
中指定
CreateType
來達到同樣的效果,如程式
22
所示。
程式
22
static
void UseDependencyAttribute(MyBuilderContext context)
{
context.InnerChain.Add(new CreationStrategy());
context.InnerChain.Add(new MethodReflectionStrategy());
context.InnerChain.Add(new MethodExecutionStrategy());
}
public
class InputAccept
{
private IDataProcessor _dataProcessor;
[InjectionMethod]
public void SetDataProcessor(
[Dependency(Name="dataProcessor",
CreateType=typeof(PromptDataProcessor))]IDataProcessor dataProcessor)
{
_dataProcessor = dataProcessor;
}
.............
}
|
4-3
、
Setter Injection
ObjectBuilder
使用
PropertySetterStrategy
來進行
Setter Injectio
n
,用法與前述的Interface Injection模式大致相同,如程式23所示。
程式23
static
void UsePropertySetter(MyBuilderContext context)
{
context.InnerChain.Add(new CreationStrategy());
context.InnerChain.Add(new PropertySetterStrategy());
PropertySetterPolicy policy = new PropertySetterPolicy();
policy.Properties.Add("DataProcessor", new PropertySetterInfo("DataProcessor",
new
ValueParameter(typeof(IDataProcessor), new PromptDataProcessor())));
context.Policies.Set<IPropertySetterPolicy>(policy, typeof(InputAccept), null);
}
static
void Main(string[] args)
{
MyBuilderContext context = new MyBuilderContext(new Locator());
UsePropertySetter(context);
context.Policies.SetDefault<ICreationPolicy>(new DefaultCreationPolicy());
InputAccept accept = (InputAccept)context.HeadOfChain.BuildUp(context,
typeof
(InputAccept), null, null);
accept.Execute();
Console.Read();
}
public
class InputAccept
{
private IDataProcessor _dataProcessor;
public IDataProcessor DataProcessor
{
get
{
return _dataProcessor;
}
set
{
_dataProcessor = value;
}
}
public void Execute()
{
Console.Write("Please Input some words:");
string input = Console.ReadLine();
input = _dataProcessor.ProcessData(input);
Console.WriteLine(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 Wor
k
?
當
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
void UseDependencyParameter(MyBuilderContext context)
{
context.InnerChain.Add(new CreationStrategy());
context.InnerChain.Add(new PropertySetterStrategy());
PropertySetterPolicy policy = new PropertySetterPolicy();
policy.Properties.Add("DataProcessor", new PropertySetterInfo("DataProcessor",
new DependencyParameter(typeof(IDataProcessor),"DataProcessor",
typeof
(PromptDataProcessor),NotPresentBehavior.CreateNew,SearchMode.Local)));
context.Policies.Set<IPropertySetterPolicy>(policy, typeof(InputAccept), null);
}
|
use PropertyReflectionStrategy
相對於
ConsturctorReflectionStrategy
及
MethodReflectionStrateg
y
,ObjectBuilder也提供了一個同類型的PropertyReflectionStrategy,我們可以搭配Dependency Attribute及DependencyResolutionLocatorKey物件來達到同樣效果,如程式25。
程式25
static
void UseDependencyResolutionLocator(MyBuilderContext context)
{
context.InnerChain.Add(new CreationStrategy());
context.InnerChain.Add(new PropertyReflectionStrategy());
context.InnerChain.Add(new PropertySetterStrategy());
context.Locator.Add(new DependencyResolutionLocatorKey(typeof(IDataProcessor),
"DataProcessor"
), new PromptDataProcessor());
}
public
class InputAccept
{
private IDataProcessor _dataProcessor;
[Dependency(Name="DataProcessor")]
public IDataProcessor DataProcessor
{
get
{
return _dataProcessor;
}
set
{
_dataProcessor = value;
}
}
.........
}
|
Injection with Dependency Attribute and CreateType
我們也可以使用
Dependency Attribute
及
CreateType
參數來進行
Setter Injectio
n
,如程式26
。
程式
26
static
void UseDependencyAttribute(MyBuilderContext context)
{
context.InnerChain.Add(new CreationStrategy());
context.InnerChain.Add(new PropertyReflectionStrategy());
context.InnerChain.Add(new PropertySetterStrategy());
}
public
class InputAccept
{
private IDataProcessor _dataProcessor;
[Dependency(Name="DataProcessor",CreateType=typeof(PromptDataProcessor))]
public IDataProcessor DataProcessor
{
get
{
return _dataProcessor;
}
set
{
_dataProcessor = value;
}
}
...............
}
|