1. 程式人生 > >依賴注入(C#)

依賴注入(C#)

一,依賴注入(Dependency Injection,簡稱DI)

設計模式中,尤其是結構型模式很多時候解決的就是物件間的依賴關係,變依賴關係具體為依賴具象。平時開發中如果發現客戶程式依賴某個(或某類)物件,我們常常會對它們進行一次抽象,形成抽象的抽象類、介面,這樣客戶程式就可以擺脫所依賴的具體型別。

二,實現(C#)

示例情景

假設,現在程式需要一個獲取不同時間格式的的當前時間。
我們定義一個介面ITimeProvider。

    public interface ITimeProvider
    {
        DateTime CurrentDate { get
; } }

ITimeProvider的不同實現,提供不同的時間格式。

    public class SystemTimeProvider : ITimeProvider
    {
        public DateTime CurrentDate { get { return DateTime.Now; } }
    }

    public class UtcNowTimeProvider : ITimeProvider
    {
        public DateTime CurrentDate { get { return DateTime.UtcNow; } }
    }

需要一個裝配工Assembler將所需的型別名稱和實體型別一一對應。

    public class Assembler
    {
        /// <summary>
        /// 儲存“型別名稱/實體型別”對應關係的字典
        /// </summary>
        private static Dictionary<string, Type> dictionary = new Dictionary<string, Type>();

        // 實際的配置資訊可以從外層機制獲得,例如通過xml檔案配置.
        static
Assembler() { dictionary.Add("SystemTimeProvider", typeof(SystemTimeProvider)); dictionary.Add("UtcNowTimeProvider", typeof(UtcNowTimeProvider)); } static void RegisterType(string name,Type type) { if ((type == null) || dictionary.ContainsKey(name)) throw new NullReferenceException(); dictionary.Add(name, type); } static void Remove(string name) { if (string.IsNullOrEmpty(name)) throw new NullReferenceException(); dictionary.Remove(name); } /// <summary> /// 根據程式需要的型別名稱選擇相應的實體型別,並返回型別例項 /// </summary> public ITimeProvider Create(string type) { if ((type == null) || !dictionary.ContainsKey(type)) throw new NullReferenceException(); Type targetType = dictionary[type]; return (ITimeProvider)Activator.CreateInstance(targetType); } }

此時出現一個問題,程式如何獲取所需的時間格式的實現類呢?通過注入的方式。
注入的方式有以下幾種。

建構函式(Constructor)注入

建構函式注入,顧名思義,就是在建構函式的時候,通過Assembler或其它機制把抽象型別作為返回給所需的程式。

    /// <summary>
    /// 在建構函式中注入
    /// </summary>
    class Client
    {
        private ITimeProvider timeProvider;

        public Client(ITimeProvider timeProvider)
        {
            this.timeProvider = timeProvider;
        }
    }

    class ConstructorInjectionTest
    {
        public static void Test()
        {
            ITimeProvider timeProvider = (new Assembler()).Create("SystemTimeProvider");
            Client client = new Client(timeProvider);   // 在建構函式中注入
        }
    }

Setter注入

Setter注入是通過屬性複製的方法解決的,由於Java等很多語言中沒有真正的屬性,所以Setter注入一般通過一個set()方法實現,C#語言由於本身就有可寫屬性,所以實現起來更簡潔,更像Setter。想比較Constructor方式而言,Setter給了客戶型別後續修改的機會,它比較適合客戶型別例項存活時間較長,但Assembler修改抽象型別指定的具體型別相對較快的情景;不過也可以由客戶程式根據需要動態設定所需的型別。

    /// <summary>
    /// 通過Setter實現注入
    /// </summary>
    class Client
    {
        private ITimeProvider timeProvider;

        public ITimeProvider TimeProvider
        {
            get { return this.timeProvider; }   // getter本身和Setter方式實現注入沒有關係
            set { this.timeProvider = value; }
        }
    }

    class SetterInjectionTest
    {
        public static void Test()
        {
            ITimeProvider timeProvider = (new Assembler()).Create("SystemTimeProvider");
            Client client = new Client();
            client.TimeProvider = timeProvider; // 通過Setter實現注入
        }
    }

介面注入

介面注入是將包括抽象型別注入的入口以方法的形式定義在一個藉口裡,如果客戶型別需要實現這個注入過程,則實現這個介面,客戶型別自己考慮如何把抽象型別引入內部。實際上介面注入有很強的侵入性,除了要求客戶型別增加需要的Setter或Constructor注入的程式碼外,還顯示地定義了一個新的介面並要求客戶型別實現它。除非還有更外層容器使用的要求,或者有完善的配置系統,可以通過反射動態實現介面注入方式注入,否則並不建議採用介面注入方式。

    /// <summary>
    /// 定義需要注入ITimeProvider的型別
    /// </summary>
    interface IObjectWithTimeProvider
    {
        ITimeProvider TimeProvider { get; set; }
    }

    /// <summary>
    /// 通過介面方式注入
    /// </summary>
    class Client : IObjectWithTimeProvider
    {
        private ITimeProvider timeProvider;

        /// <summary>
        /// IObjectWithTimeProvider Members
        /// </summary>
        public ITimeProvider TimeProvider
        {
            get { return this.timeProvider; }
            set { this.timeProvider = value; }
        }
    }

    class InterfacerInjectionTest
    {
        public static void Test()
        {
            ITimeProvider timeProvider = (new Assembler()).Create("SystemTimeProvider");
            IObjectWithTimeProvider objectWithTimeProvider = new Client();
            objectWithTimeProvider.TimeProvider = timeProvider; // 通過介面方式注入
        }
    }

基於Attribute實現注入

可以通過Attribute將附加的內容注入到物件上。

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    sealed class DecoratorAttribute : Attribute
    {

        public readonly object Injector;
        private Type type;

        public DecoratorAttribute(Type type)
        {
            if (type == null) throw new ArgumentNullException("type");
            this.type = type;
            Injector = (new Assembler()).Create("SystemTimeProvider");
        }

        public Type Type { get { return this.type; } }
    }

    /// <summary>
    /// 使用者幫助客戶型別和客戶程式獲取其Attribute定義中需要的抽象型別例項
    /// </summary>
    static class AttributeHelper
    {
        public static T Injector<T>(object target)
            where T : class
        {
            if (target == null) throw new ArgumentNullException("target");
            Type targetType = target.GetType();
            object[] attributes = targetType.GetCustomAttributes(typeof(DecoratorAttribute), false);
            if ((attributes == null) || (attributes.Length <= 0)) return null;
            foreach (DecoratorAttribute attribute in (DecoratorAttribute[])attributes)
                if (attribute.Type == typeof(T))
                    return (T)attribute.Injector;
            return null;
        }
    }

    [Decorator(typeof(ITimeProvider))]
    class Client
    {
        public string GetTime()
        {
            // 與其他方式注入不同的是,這裡使用的ITimeProvider來自自己的Attribute
            ITimeProvider provider = AttributeHelper.Injector<ITimeProvider>(this);
            return provider.CurrentDate.ToString();
        }
    }

    class AttributerInjectionTest
    {
        public static void Test()
        {
            Client client = new Client();
            string time = client.GetTime();
        }
    }

三,小結

依賴注入各種實現方式對比:
建構函式(Constructor)注入方式:它的注入是一次性的,當客戶型別構造的時候就確定了。它很適合那種生命週期不長的物件,比如在其存續期間不需要重啟適配的物件。另外,相對Setter方式而言,在實現上Constructor可以節省很多程式碼。
Setter方式:一個很靈活的實現方式,對於生命週期較長的客戶物件而言,可以在執行過程中隨時適配。
介面方式:作為注入方式具有侵入性,很多成都上它適合需要同時約束一批客戶型別的情況。
屬性方式:本身具有範圍較小的固定內容侵入性(一個Attribute),它適合需要同時約束一批客戶型別的情景。它本身實現相對複雜一些,但客戶型別使用的時候非常方便,打標籤即可。

四,參考資料

《設計模式:基於C#的工程化實現及擴充套件》