Inside ObjectBuilder Part1

摘要:Object Builder Application Block

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
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("Please Input Some Words:");
            string inputData = Console.ReadLine();
            Console.WriteLine(inputData);
            Console.Read();
        }
    }
}
從整個流程上看來,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
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            MessageBox.Show(textBox1.Text);
        }
    }
}
與程式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更能描述現今許多宣稱支援IoCFramework內部的行為,在Martin Fowler的解釋中, Dependency Injection分成三種,一是Interface Injection(介面注射)Constructor Injection(建構子注射)Setter Injection(設值注射)
 
2-1Why we need Dependency Injection
 
 OK,花了許多篇幅在解釋IoCDependency Injection兩個概念,希望讀者們已經明白這兩個名詞的涵意,在切入Dependency Injection這個主題前,我們要先談談為何要使用Dependency Injection,及這樣做帶來了什麼好處,先從程式3的例子開始。
程式3
using System;
using System.Collections.Generic;
using System.Text;
 
namespace DISimple
{
    class Program
    {
        static void Main(string[] args)
        {
            InputAccept accept = new InputAccept(new PromptDataProcessor());
            accept.Execute();
            Console.ReadLine();
        }
    }
 
    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
    }
}
這是一個簡單且無用的例子,但卻可以告訴我們為何要使用Dependency Injection,在這個例子中,必須在建立InputAccept物件時傳入一個實作IDataProcessor介面的物件,這是Interface Base Programming概念的設計模式,這樣做的目的是為了降低InputAccept與實作體間的耦合關係,重用InputAccept的執行流程,以此來增加程式的延展性。那這個設計有何不當之處呢?沒有!問題不在InputAcceptIDataProcessor的設計,而在於使用的方式。
InputAccept accept = new InputAccept(new PromptDataProcessor());
使用InputAccept時,必須在建立物件時傳入一個實作IDataProcess介面的物件,此處直接建立一個PromptDataProcessor物件傳入,這使得主程式與PromptDataProcessor物件產生了關聯性,間接的摧毀使用IDataProcessor時所帶來的低耦合性,那要如何解決這個問題呢?讀過Design Patterns的讀者會提出以BuilderFactory等樣式解決這個問題,如下所示。
//Factory
InputAccept accept = new InputAccept(DataProcessorFactory.Create());
//Builder
InputAccept accept = new InputAccept(DataProcessorBulder.Build());
兩者的實際流程大致相同,DataProcessorFactory.Create函式會依據組態檔的設定來建立指定的IDataProcessor實作體,回傳後指定給InputAcceptDataProcessBuilder.Build函式所做的事也大致相同。這樣的設計是將原本位於主程式中IDataProcessor物件的建立動作,轉移到DataProcessorFactoryDataProcessorBuilder上,這也算是一種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的關聯,由於只使用一個FactoryGenericFactory, 所以不存在於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-2Interface Injection
 
 Interface Injection指的是將原本建構於物件間的依賴關係,轉移到一個介面上,程式4是一個簡單的例子。
程式4
using System;
using System.Collections.Generic;
using System.Text;
 
namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            InputAccept accept = new InputAccept();
            accept.Inject(new DummyDataProcessor());
            accept.Execute();
            Console.Read();
        }
    }
 
    public class InputAccept
    {
        private IDataProcessor _dataProcessor;
 
        public void Inject(IDataProcessor dataProcessor)
        {
            _dataProcessor = dataProcessor;
        }
 
        public void Execute()
        {
            Console.Write("Please Input some words:");
            string input = Console.ReadLine();
            input = _dataProcessor.ProcessData(input);
            Console.WriteLine(input);
        }
    }
 
    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
    }
}
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-3Constructor Injection
 
 Constructor Injection意指建構子注入,主要是利用建構子參數來注入依賴關係,建構子注入通常是與容器緊密相關的,容器允許設計者透過特定函式,將欲注入的物件事先放入容器中,當使用端要求一個支援建構子注入的物件時,容器中會依據目標物件的建構子參數,一一將已放入容器中的物件注入。程式6是一個簡單的容器類別,其支援Constructor Injection
程式6
public static class Container
{
        private static Dictionary<Type, object> _stores = null;
 
        private static Dictionary<Type, object> Stores
        {
            get
            {
                if (_stores == null)
                    _stores = new Dictionary<Type, object>();
                return _stores;
            }
        }
 
        private static Dictionary<string,object> CreateConstructorParameter(Type targetType)
        {
            Dictionary<string, object> paramArray = new Dictionary<string, object>();
 
            ConstructorInfo[] cis = targetType.GetConstructors();
            if (cis.Length > 1)
                throw new Exception(
"target object has more then one constructor,container can't peek one for you.");          
 
            foreach (ParameterInfo pi in cis[0].GetParameters())
            {
                if (Stores.ContainsKey(pi.ParameterType))
                    paramArray.Add(pi.Name, GetInstance(pi.ParameterType));
            }
            return paramArray;
        }
 
        public static object GetInstance(Type t)
        {
            if (Stores.ContainsKey(t))
            {
                ConstructorInfo[] cis = t.GetConstructors();
                if (cis.Length != 0)
                {
                    Dictionary<string, object> paramArray = CreateConstructorParameter(t);
                    List<object> cArray = new List<object>();
                    foreach (ParameterInfo pi in cis[0].GetParameters())
                    {
                        if (paramArray.ContainsKey(pi.Name))
                            cArray.Add(paramArray[pi.Name]);
                        else
                            cArray.Add(null);
                    }
                    return cis[0].Invoke(cArray.ToArray());
                }
                else if (Stores[t] != null)
                    return Stores[t];
                else
                    return Activator.CreateInstance(t, false);
            }
            return Activator.CreateInstance(t, false);
        }
 
        public static void RegisterImplement(Type t, object impl)
        {
            if (Stores.ContainsKey(t))
                Stores[t] = impl;
            else
                Stores.Add(t, impl);
        }
 
        public static void RegisterImplement(Type t)
        {
            if (!Stores.ContainsKey(t))
                Stores.Add(t, null);
        }
}
Container類別提供了兩個函式,RegisterImplement有兩個重載函式,一接受一個Type物件及一個不具型物件,她會將傳入的Type及物件成對的放入Stores這個Collection中,另一個重載函式則只接受一個Type物件,呼叫這個函式代表呼叫端不預先建立該物件,交由GetInstance函式來建立。GetInstance函式負責建立物件,當要求的物件型別存在於Stores記錄中時,其會取得該型別的建構子,並依據建構子的參數,一一呼叫GetInstance函式來建立物件。程式7是使用這個Container的範例。
程式7
class Program
{
        static void Main(string[] args)
        {
            Container.RegisterImplement(typeof(InputAccept));
            Container.RegisterImplement(typeof(IDataProcessor), new PromptDataProcessor());
            InputAccept accept = (InputAccept)Container.GetInstance(typeof(InputAccept));
            accept.Execute();
            Console.Read();
        }
}
 
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
}
 
2-4Setter Injection
 
 Setter Injection意指設值注入,主要概念是透過屬性的途徑,將依賴物件注入目標物件中,與Constructor Injection模式一樣,這個模式同樣需要容器的支援,程式8是支援Setter InjectionContainer程式列表。
程式8
public static class Container
    {
        private static Dictionary<Type, object> _stores = null;
 
        private static Dictionary<Type, object> Stores
        {
            get
            {
                if (_stores == null)
                    _stores = new Dictionary<Type, object>();
                return _stores;
            }
        }
 
        public static object GetInstance(Type t)
        {
            if (Stores.ContainsKey(t))
            {
                if (Stores[t] == null)
                {
                    object target = Activator.CreateInstance(t, false);
                    foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(target))
                    {
                        if (Stores.ContainsKey(pd.PropertyType))
                            pd.SetValue(target, GetInstance(pd.PropertyType));
                    }
                    return target;
                }
                else
                    return Stores[t];
            }
            return Activator.CreateInstance(t, false);
        }
 
        public static void RegisterImplement(Type t, object impl)
        {
            if (Stores.ContainsKey(t))
                Stores[t] = impl;
            else
                Stores.Add(t, impl);
        }
 
        public static void RegisterImplement(Type t)
        {
            if (!Stores.ContainsKey(t))
                Stores.Add(t, null);
        }
    }
程式碼與Constructor Injection模式大致相同,兩者差異之處僅在於Constructor Injection是使用建構子來注入,Setter Injection是使用屬性來注入,程式9是使用此Container的範例。
程式9
class Program
    {
        static void Main(string[] args)
        {
            Container.RegisterImplement(typeof(InputAccept));
           Container.RegisterImplement(typeof(IDataProcessor), new PromptDataProcessor());
            InputAccept accept = (InputAccept)Container.GetInstance(typeof(InputAccept));
            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);
        }
    }
 
2-5Service Locator
 
 Martain Fowler的文章中,Dependency Injection並不是唯一可以將物件依賴關係降低的方式,另一種Service Locator架構也可以達到同樣的效果,從架構角度來看,Service Locator是一個服務中心,設計者預先將Servcie物件推入Locator容器中,在這個容器內,Service是以Key/Value方式存在。欲使用該Service物件的物件,必須將依賴關係建立在Service Locator上,也就是說,不是透過建構子、屬性、或是方法來取得依賴物件,而是透過Service Locator來取得。