[.NET] Rough Dependency Injection

摘要:[.NET] Rough Dependency Injection

動機

在設計系統架構的時候,在系統裡加入Dependency Injection(DI),讓系統可以在不改變程式碼的狀況下,抽換類別來提高重用性、擴充性。在.NET裡可以選擇一些的Framework來使用,例如:Spring Framework、Unity Application Block、Managed Extensibility Framework (MEF)。


在一些中小型專案,套用上列這些Framework,常常會有種拿大砲打蚊子的感覺。因為這些Framework為了能夠符合更多的使用情景,而加入了很多功能。一直加下去的結果,就是系統變的龐大並且較難理解上手。以Spring來說,光是怎麼寫設定檔的操作,都可以寫成書了。當然這樣用意是好的,一個夠強大的Framework學習曲線必然會較高,但是學會之後能適用的範圍也會更廣。


不過不得面對的現實是,很多開發人員沒時間學習各種Framework(或是無心學習?)。在系統架構裡加入這些強大Framework,提高了系統的各項品質時,也拉高技術門檻。過高的技術門檻在後續開發、維護,補充人手時會越來越艱難…。以此為發想於是就萌生了:建立簡單易用的一組類別,完成Dependency Injection(DI)應該具備的基礎功能。只需要學習基礎功能,就能為系統加入Dependency Injection(DI)功能。這樣就能降低開發人員的技術門檻,讓更多的開發人員能做為補充人力加入專案。


本篇文章介紹一個實作Dependency Injection(DI)基礎功能的Rough Dependency Injection實作,這個實作定義物件之間的職責跟互動,用來反射生成要注入的物件。為自己做個紀錄,也希望能幫助到有需要的開發人員。


* 必須要特別聲明的是,Spring、MEF這些Framework有很高的價值。當這些Framework成為整個團隊基礎開發知識時,整個團隊的開發能力將會提升一個台階。


結構

Rough Dependency Injection主要是將Dependency Injection(DI) 基礎功能,拆為兩大部分:物件生成、物件設定,並且將複雜的物件設定隔離在系統之外。模式的結構如下:



主要的參與者有:


TEntity
-欲注入系統的物件。


IReflectProfileRepository
- ReflectProfile進出系統邊界的介面。
-將物件設定隔離在系統之外,可以抽換各種不同資料存儲。
-極端的案例可以抽換成為HardCodeRepository,連設定都從系統移除。


ReflectProfile
-DTO物件。
-儲存用來反射生成IReflectBuilder所需要的參數。


IReflectBuilder
-經由ReflectProfile儲存參數,反射生成的物件介面。
-使用ReflectProfile儲存參數,生成TEntity。


ReflectManager
-藉由IReflectProfileRepository取得系統儲存的ReflectProfile。
-使用ReflectProfile儲存參數,反射生成IReflectBuilder。
-使用IReflectBuilder與ReflectProfile,生成TEntity。


透過下面的圖片說明,可以了解相關物件之間的互動流程。



實作

範列下載

實作說明請參照範例程式內容:RoughDependencyInjectionSample點此下載


ReflectProfile、IReflectProfileRepository

首先為了將物件設定這個職責,隔離在系統之外。所以在整個模組的邊界套用Repository模式,建立出IReflectProfileRepository。並且使用ReflectProfile做為進出系統邊界的DTO物件,這個ReflectProfile物件儲存生成物件的參數資料。



namespace CLK.Reflection
{
    public sealed class ReflectProfile
    {
        // Constructors
        public ReflectProfile()
        {
            // Default
            this.ProfileName = string.Empty;
            this.BuilderType = string.Empty;
            this.Parameters = new Dictionary<string, string>();
        }


        // Properties
        public string ProfileName { get; set; }

        public string BuilderType { get; set; }

        public Dictionary<string, string> Parameters { get; private set; }
    }
}


namespace CLK.Reflection
{
    public interface IReflectProfileRepository
    {
        // Methods
        string GetDefaultProfileName(string reflectSpace);

        ReflectProfile GetProfile(string reflectSpace, string profileName);

        IEnumerable<ReflectProfile> CreateAllProfile(string reflectSpace);
    }
}

IReflectBuilder、ReflectManager

接著處理ReflectManager來使用ReflectProfile。ReflectManager主要的工作就是透過IReflectProfileRepository取得的ReflectProfile,用ReflectProfile來反射生成IReflectBuilder實作。然後在利用這個IReflectBuilder實作,配合ReflectProfile來生成系統需要注入的TEntity。之所以不直接反射生成TEntity另外再建一層IReflectBuilder,是因為不希望在TEntity裡,混入DI相關功能的相依。



namespace CLK.Reflection
{
    public interface IReflectBuilder
    {
        // Methods
        object Create(Dictionary<string, string> parameters);
    }
}


namespace CLK.Reflection
{
    public class ReflectManager
    {
        // Fields
        private readonly IReflectProfileRepository _repository = null;


        // Constructors 
        public ReflectManager(IReflectProfileRepository repository)
        {
            #region Contracts

            if (repository == null) throw new ArgumentNullException();

            #endregion

            // Arguments
            _repository = repository;
        }


        // Methods
        private TEntity Create<TEntity>(ReflectProfile profile) where TEntity : class
        {
            #region Contracts

            if (profile == null) throw new ArgumentNullException();

            #endregion

            // Require
            if (string.IsNullOrEmpty(profile.ProfileName) == true) throw new InvalidOperationException();
            if (string.IsNullOrEmpty(profile.BuilderType) == true) throw new InvalidOperationException();

            // BuilderType
            Type builderType = Type.GetType(profile.BuilderType);
            if (builderType == null) throw new ArgumentException(String.Format("Action:{0}, State:{1}, BuilderType:{2}", "Reflect", "Fail to Access BuilderType", profile.BuilderType));

            // Builder
            IReflectBuilder builder = Activator.CreateInstance(builderType) as IReflectBuilder;
            if (builder == null) throw new ArgumentException(String.Format("Action:{0}, State:{1}, BuilderType:{2}", "Reflect", "Fail to Create Builder", profile.BuilderType));
            
            // Entity
            TEntity entity = builder.Create(profile.Parameters) as TEntity;
            if (entity == null) throw new ArgumentException(String.Format("Action:{0}, State:{1}, BuilderType:{2}", "Reflect", "Fail to Create Entity", profile.BuilderType));

            // Return
            return entity;
        }


        public IEnumerable<TEntity> CreateAll<TEntity>(string reflectSpace) where TEntity : class
        {
            #region Contracts

            if (string.IsNullOrEmpty(reflectSpace) == true) throw new ArgumentNullException();

            #endregion

            // Result
            List<TEntity> entityList = new List<TEntity>();

            // Create
            foreach (ReflectProfile profile in _repository.CreateAllProfile(reflectSpace))
            {
                TEntity entity = this.Create<TEntity>(profile);
                if (entity != null)
                {
                    entityList.Add(entity);
                }
            }

            // Return
            return entityList;
        }

        public TEntity Create<TEntity>(string reflectSpace) where TEntity : class
        {
            #region Contracts

            if (string.IsNullOrEmpty(reflectSpace) == true) throw new ArgumentNullException();

            #endregion

            // ProfileName
            string profileName = _repository.GetDefaultProfileName(reflectSpace);
            if (string.IsNullOrEmpty(profileName) == true) throw new ArgumentException(String.Format("Action:{0}, State:{1}, ReflectSpace:{2}", "Reflect", "Fail to Get DefaultProfileName", reflectSpace));

            // Return
            return this.Create<TEntity>(reflectSpace, profileName);
        }

        public TEntity Create<TEntity>(string reflectSpace, string profileName) where TEntity : class
        {
            #region Contracts

            if (string.IsNullOrEmpty(reflectSpace) == true) throw new ArgumentNullException();
            if (string.IsNullOrEmpty(profileName) == true) throw new ArgumentNullException();

            #endregion

            // Profile
            ReflectProfile profile = _repository.GetProfile(reflectSpace, profileName);
            if (profile == null) return default(TEntity);

            // Return
            return this.Create<TEntity>(profile);
        }        
    }    
}

ConfigReflectProfileRepository

在範例程式裡,示範了IReflectProfileRepository的實作,這個實作使用App.config做為ReflectProfile的資料來源。相關的程式碼如下,有興趣的開發人員可以花點時間學習,在需要擴充IReflectProfileRepository的時候(例如:改用SQL存放),就可以自行加入相關的實作。



namespace CLK.Reflection.Implementation
{
    public class ConfigReflectProfileRepository : IReflectProfileRepository
    {
        // Default
        public static string GetDefaultConfigFilename()
        {
            // Configuration
            System.Configuration.Configuration configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
            if (configuration == null) throw new ArgumentNullException();

            // ConfigFilename
            string configFilename = configuration.FilePath;
            if (string.IsNullOrEmpty(configFilename) == true) throw new ArgumentNullException();

            // Return
            return configFilename;
        }


        // Fields
        private readonly string _configFilename = null;

        private System.Configuration.Configuration _configuration = null;


        // Constructors
        public ConfigReflectProfileRepository() : this(ConfigReflectProfileRepository.GetDefaultConfigFilename()) { }

        public ConfigReflectProfileRepository(string configFilename)
        {
            #region Require

            if (string.IsNullOrEmpty(configFilename) == true) throw new ArgumentNullException();

            #endregion

            // ConfigFilename
            _configFilename = configFilename;
        }


        // Methods
        private System.Configuration.Configuration CreateConfiguration()
        {
            if (_configuration == null)
            {
                if (_configFilename != null)
                {
                    ExeConfigurationFileMap configFileMap = new ExeConfigurationFileMap();
                    configFileMap.ExeConfigFilename = _configFilename;
                    _configuration = ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None);
                }
                else
                {
                    _configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
                }
            }
            return _configuration;
        }

        private ConfigReflectProfileSection CreateSection(string reflectSpace)
        {
            #region Require

            if (string.IsNullOrEmpty(reflectSpace) == true) throw new ArgumentNullException();

            #endregion

            // Configuration
            System.Configuration.Configuration configuration = this.CreateConfiguration();
            if (configuration == null) throw new ArgumentException(String.Format("Action:{0}, State:{1}", "Reflect", "Fail to Create Configuration"));

            // Section
            ConfigReflectProfileSection section = configuration.GetSection(reflectSpace) as ConfigReflectProfileSection;
            if (section == null) throw new ArgumentException(String.Format("Action:{0}, State:{1}, ReflectSpace:{2}", "Reflect", "Fail to Create ConfigReflectProfileSection", reflectSpace));
            
            // Return
            return section;
        }
        

        public string GetDefaultProfileName(string reflectSpace)
        {
            #region Require

            if (string.IsNullOrEmpty(reflectSpace) == true) throw new ArgumentNullException();

            #endregion

            // Section
            ConfigReflectProfileSection section = this.CreateSection(reflectSpace);
            if (section == null) throw new MemberAccessException();

            // DefaultProfileName
            string defaultProfileName = section.DefaultProfileName;
            if (string.IsNullOrEmpty(defaultProfileName) == true) throw new ArgumentException(String.Format("Action:{0}, State:{1}, ReflectSpace:{2}", "Reflect", "Fail to Get DefaultProfileName", reflectSpace));
            
            // Return
            return defaultProfileName;
        }

        public ReflectProfile GetProfile(string reflectSpace, string profileName)
        {
            #region Require

            if (string.IsNullOrEmpty(reflectSpace) == true) throw new ArgumentNullException();

            #endregion

            // Result
            ReflectProfile profile = null;

            // Section
            ConfigReflectProfileSection section = this.CreateSection(reflectSpace);
            if (section == null) throw new MemberAccessException();

            // Element
            foreach (ConfigReflectProfileElement element in section.ProfileCollection)
            {
                if (element.ProfileName == profileName)
                {
                    // Profile
                    profile = new ReflectProfile();
                    profile.ProfileName = element.ProfileName;
                    profile.BuilderType = element.BuilderType;
                    foreach (string parameterKey in element.UnrecognizedAttributes.Keys)
                    {
                        profile.Parameters.Add(parameterKey, element.UnrecognizedAttributes[parameterKey]);
                    }
                    break;
                }
            }

            // Return
            return profile;
        }

        public IEnumerable<ReflectProfile> CreateAllProfile(string reflectSpace)
        {
            #region Require

            if (string.IsNullOrEmpty(reflectSpace) == true) throw new ArgumentNullException();

            #endregion

            // Result
            List<ReflectProfile> profileList = new List<ReflectProfile>();;

            // Section
            ConfigReflectProfileSection section = this.CreateSection(reflectSpace);
            if (section == null) throw new MemberAccessException();

            // Element
            foreach (ConfigReflectProfileElement element in section.ProfileCollection)
            {
                    // Profile
                     ReflectProfile profile = new ReflectProfile();
                    profile.ProfileName = element.ProfileName;
                    profile.BuilderType = element.BuilderType;
                    foreach (string parameterKey in element.UnrecognizedAttributes.Keys)
                    {
                        profile.Parameters.Add(parameterKey, element.UnrecognizedAttributes[parameterKey]);
                    }
                profileList.Add(profile);
            }

            // Return
            return profileList;
        }
    }
}


namespace CLK.Reflection.Implementation
{
    public sealed class ConfigReflectProfileSection : ConfigurationSection
    {
        // Properties
        [ConfigurationProperty("default", DefaultValue = "", IsRequired = false)]
        public string DefaultProfileName
        {
            get
            {
                return (string)base["default"];
            }
            set
            {
                base["default"] = value;
            }
        }

        [ConfigurationProperty("", IsDefaultCollection = true, IsRequired = false)]
        public ConfigReflectProfileElementCollection ProfileCollection
        {
            get
            {
                return (ConfigReflectProfileElementCollection)base[""];
            }
        }
    }

    public sealed class ConfigReflectProfileElementCollection : ConfigurationElementCollection
    {
        // Constructor
        public ConfigReflectProfileElementCollection() { }


        // Properties
        public override ConfigurationElementCollectionType CollectionType
        {
            get
            {
                return ConfigurationElementCollectionType.AddRemoveClearMap;
            }
        }


        // Methods  
        protected override ConfigurationElement CreateNewElement()
        {
            return new ConfigReflectProfileElement();
        }

        protected override object GetElementKey(ConfigurationElement element)
        {
            #region Contracts

            if (element == null) throw new ArgumentNullException();

            #endregion
            return ((ConfigReflectProfileElement)element).ProfileName;
        }


        public void Add(ConfigReflectProfileElement element)
        {
            #region Contracts

            if (element == null) throw new ArgumentNullException();

            #endregion
            this.BaseAdd(element);
        }

        public void Remove(string name)
        {
            this.BaseRemove(name);
        }        

        public bool Contains(string name)
        {
            #region Contracts

            if (string.IsNullOrEmpty(name) == true) throw new ArgumentNullException();

            #endregion
            if (this.BaseGet(name) != null)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public ConfigReflectProfileElement GetByName(string name)
        {
            #region Contracts

            if (string.IsNullOrEmpty(name) == true) throw new ArgumentNullException();

            #endregion
            return (ConfigReflectProfileElement)this.BaseGet(name);
        }
    }

    public sealed class ConfigReflectProfileElement : UnrecognizedAttributeConfigurationElement
    {
        // Constructor
        public ConfigReflectProfileElement()
        {
            // Default
            this.ProfileName = string.Empty;
            this.BuilderType = string.Empty;
        }


        // Properties
        [ConfigurationProperty("name", IsKey = true, IsRequired = true)]
        public string ProfileName
        {
            get
            {
                return Convert.ToString(base["name"]);
            }
            set
            {
                base["name"] = value;
            }
        }

        [ConfigurationProperty("builderType", IsKey = true, IsRequired = false)]
        public string BuilderType
        {
            get
            {
                return Convert.ToString(base["builderType"]);
            }
            set
            {
                base["builderType"] = value;
            }
        }
    }
}

使用

DisplayWorker

接著撰寫一個虛擬的IDisplayWorker來示範如何套用Rough Dependency Injection。首先在專案內建立IDisplayWorker,這個IDisplayWorker很簡單的只開放一個Show函式讓系統使用。接著建立兩個實作IDisplayWorker的物件,這兩個物件就是後續要用來注入系統的物件。到這邊可以發現,套用Rough Dependency Injection注入的物件,不會有額外的相依,只要完成自己應有的職責就可以。



namespace TestProject
{
    public interface IDisplayWorker
    {
        // Methods
        void Show();
    }

    public class AAADisplayWorker : IDisplayWorker
    {
        // Properties
        public string AAA { get; set; }


        // Methods
        public void Show()
        {
            Console.WriteLine(this.AAA);
        }
    }

    public class BBBDisplayWorker : IDisplayWorker
    {
        // Properties
        public int BBB { get; set; }


        // Methods
        public void Show()
        {
            Console.WriteLine(this.BBB);
        }
    }
}

DisplayWorkerBuilder

要讓注入物件,不會有額外的相依,也是要付出代價。要另外建立一層Builder,用來生成注入物件,以及隔離注入物件與Rough Dependency Injection的相依。



namespace TestProject
{
    public class AAADisplayWorkerBuilder : IReflectBuilder
    {
        // Methods
        public object Create(Dictionary<string, string> parameters)
        {
            AAADisplayWorker worker = new AAADisplayWorker();
            worker.AAA = Convert.ToString(parameters["AAA"]);
            return worker;
        }
    }

    public class BBBDisplayWorkerBuilder : IReflectBuilder
    {
        // Methods
        public object Create(Dictionary<string, string> parameters)
        {
            BBBDisplayWorker worker = new BBBDisplayWorker();
            worker.BBB = Convert.ToInt32(parameters["BBB"]);
            return worker;
        }
    }
}

執行

最後建立使用IDisplayWorker的範例RoughDependencyInjectionSample,在RoughDependencyInjectionSample內透過ReflectManager搭配App.config裡的設定,為系統注入兩個IDisplayWorker實作讓系統使用。



<?xml version="1.0" encoding="utf-8" ?>
<configuration>

  <!-- ConfigSections -->
  <configSections>
    <sectionGroup name="testProject">
      <section name="displayWorker" type="CLK.Reflection.Implementation.ConfigReflectProfileSection, CLK" />
    </sectionGroup>
  </configSections>

  <!-- TestProject -->
  <testProject>
    <displayWorker>
      <add name="AAA" builderType="TestProject.AAADisplayWorkerBuilder, TestProject" AAA="Clark=_=y-~" />
      <add name="BBB" builderType="TestProject.BBBDisplayWorkerBuilder, TestProject" BBB="1234" />
    </displayWorker>
  </testProject>

</configuration>


namespace TestProject
{
    class Program
    {
        static void Main(string[] args)
        {
            // ReflectManager
            ReflectManager reflectManager = new ReflectManager(new ConfigReflectProfileRepository());

            // CreateAll
            foreach (IDisplayWorker worker in reflectManager.CreateAll<IDisplayWorker>(@"testProject/displayWorker"))
            {
                // Show
                worker.Show();
            }

            // End
            Console.ReadLine();
        }
    }
}


期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。