[.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(); } } }
期許自己~
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。