第2章 开始每个设计模式之前
2.1 new()的替代品
2.2 准备一个轻量的内存Cache
2.3 准备一个集中访问配置文件的Broker
2.4 Web?Not Web?
2.5 小结
本书沿用《设计模式:可复用面向对象软件的基础》中的标准分类原则,依据第一准则(目的)和第二准则(范围),按照表2-1归类所有模式。
表2-1
当介绍每个模式的时候,会用相对较小的篇幅介绍每个模式的意图、动机、适用性、结构和预期达到的效果,之后,着重花大力气探讨实际工程应用中如何基于.NET平台和C#语言特性,设计并实现可工程化复用的高质量模式库。其中很多考虑来自非功能性代码属性的分析,既包括开发期质量属性,也包括运行期质量属性,其目的有两个(见表2-2)。
表2-2
● 开发环境中,开发人员用着方便。
● 生产环境中,运行维护人员管理起来容易。
由于整个代码库是Class Library,本身不会主动发起线程或启动进程,因此是否能够充分利用多核环境下的处理器能力还需要依赖外部应用设计,但代码库本身必须可以适应并发。安全性编码主要包括充分利用.NET Framework CAS(Code Access Security)、RBS(Role Based Security)特性,并结合常规的安全编码常识,为了不“喧宾夺主”,本书对此不会进行专门讨论,相关内容可参考如下资源:
MSDN
Design Guidelines for Developing Class Libraries Secure Coding Guidelines Security How-to Topics Web Service Security - Scenarios, Patterns, and Implementation Guidance for Web Services Enhancements (WSE) 3.0 Building Secure ASP.NET Applications: Authentication, Authorization, and Secure Communication Improving Web Application Security: Threats and Countermeasures
为了后续章节实现方便,本章还需要准备些工具,确保后续代码在实现上不须为了重复的工程化问题,反复编码。
2.1 new()的替代品
第一个要准备的就是解决一个对象是如何构造的。我们的选择很多:
● 使用ObjectBuilder库;
● 强迫所有需要创建的对象都支持 where T : new();
● 做一个轻量级的对象构造“泵”。
第一种选择很企业级,但是可以用于业务对象,对于工程化的设计模式库而言,似乎有点本末倒置了,有些重,不过可以算作一种选择;第三种选择,Cut,对于客户应用要求太苛刻了,逼着别人“削足适履”;第三个选择,做个轻便的、Portable的Object Builder,可去掉那些Builder Strategy、Builder Policy。
客户层需要面对两个new()替代方式,其思路有点坏了“依赖于抽象,而不依赖于实现”的规矩,那么可以抽象一层,为了和使用上的习惯一样,叫IObjectBuilder好了。
但最终选谁来实现IObjectBuilder?为了不和ObjectBuilder的信徒冲突,为了不和支持被“Cut”掉的那个方案的同事发生冲突,就按照我们前面说的——写在配置中,其代码如下:
C#
using System; namespace MarvellousWorks.PracticalPattern.Common { /// <summary> /// 根据类型名称生成类型实例 /// </summary> public interface IObjectBuilder { /// <summary> /// 创建类型实例 /// </summary> /// <typeparam name="T">返回类型</typeparam> /// <param name="args">构造参数</param> /// <returns>指定类型T的实例</returns> T BuildUp<T>(object[] args); /// <summary> /// 创建类型实例 /// <remarks>该方法仅适用于有无参构造函数的类型</remarks> /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> T BuildUp<T>() where T : new(); /// <summary> /// 按照目标返回的类型,加工指定类型名称对应的类型实例。 /// 目标类型可以为接口、抽象类等抽象类型,typeName一般为实体类名称。 /// </summary> /// <typeparam name="T">目标返回的类型。</typeparam> /// <param name="typeName">类型名称<</param> /// <returns>按照目标返回类型加工好的一个实例</returns> T BuildUp<T>(string typeName); /// <summary> /// 按照目标类型,通过调用指定名称类型的构造函数,生成目标类型实例。 /// </summary> /// <typeparam name="T">目标返回的类型</typeparam> /// <param name="typeName">类型名称</param> /// <param name="args">构造函数参数</param> /// <returns>按照目标返回类型加工好的一个实例</returns> T BuildUp<T>(string typeName, object[] args); } }
App.Config
<configuration> <configSections> <sectionGroup name="marvellousWorks.practicalPattern.common" type="fake"> <section name="objectBuilder" type="fake"/> </sectionGroup> </configSections> <marvellousWorks.practicalPattern.common> <objectBuilder type="fake"/> </marvellousWorks.practicalPattern.common> </configuration>
然后,实现一个最portable的TypeCreator,该类通过调用System.Activator动态构造类型实例,其代码如下:
C#
参考Common项目中的TypeCreator。 using System; using System.Reflection; namespace MarvellousWorks.PracticalPattern.Common { /// <summary> /// 一个轻便的IObjectBuilder实现 /// </summary> public class TypeCreator : IObjectBuilder { public T BuildUp<T>() where T: new() { return Activator.CreateInstance<T>(); } public T BuildUp<T>(string typeName) { return (T)Activator.CreateInstance(Type.GetType(typeName)); } public T BuildUp<T>(object[] args) { object result=Activator.CreateInstance(typeof(T), args); return (T)result; } public T BuildUp<T>(string typeName, object[] args) { object result=Activator.CreateInstance(Type.GetType(typeName), args); return (T)result; } } }
Unit Test
参考Common.Test项目的TypeCreatorTest 相应配置文件中ObjectBuilder的配置节部分修改为: <configuration> <configSections> … … </configSections> <marvellousWorks.practicalPattern.common> <objectBuilder type="MarvellousWorks.PracticalPattern.Common.TypeCreator, MarvellousWorks.PracticalPattern.Common"/> </marvellousWorks.practicalPattern.common> </configuration>
2.2 准备一个轻量的内存Cache
Cache虽然可以简单至一个Dictionary,复杂至Enterprise Library的Cache Block,但本书涉及的内容更多的是一个轻量的线程安全的内存Cache对象(见图2-1)。本节准备了一个GenericCache用于完成这类工作。
图2-1 一个简单的内存缓冲对象
● 直接使用Dictionary<TKey, TValue>作为缓冲容器。
● 考虑到普遍而言 Cache,读频率大于写频率(否则还是别缓冲了),使用多读单写锁(System.Threading.ReaderWriterLock)控制并发同步。
● 使用上仅提供Dictionary<TKey, TValue>集合最基本的几个操作,而且没有使用Remove()方法,仅使用一个Clear()方法。
示例代码如下:
C#
using System; using System.Collections.Generic; using System.Threading; namespace MarvellousWorks.PracticalPattern.Common { /// <summary> /// 线程安全的轻量泛型类提供了从一组键到一组值的映射 /// </summary> /// <typeparam name="TKey">字典中键的类型</typeparam> /// <typeparam name="TValue">字典中值的类型</typeparam> public class GenericCache<TKey, TValue> { #region Fields /// <summary> /// 内部的Dictionary 容器 /// </summary> private Dictionary<TKey, TValue> dictionary = new Dictionary<TKey, TValue>(); /// <summary> /// 用于并发同步访问的RW 锁对象 /// </summary> private ReaderWriterLock rwLock=new ReaderWriterLock(); /// <summary> /// 一个TimeSpan,用于指定超时时间 /// </summary> private readonly TimeSpan lockTimeOut = TimeSpan.FromMilliseconds(100); #endregion #region Methods /// <summary> /// 将指定的键和值添加到字典中 /// Exceptions: /// ArgumentException - Dictionary 中已存在具有相同键的元素。 /// </summary> /// <param name="key">要添加的元素的键</param> /// <param name="value">添加的元素的值。对于引用类型,该值可以为空引用</param> public void Add(TKey key, TValue value) { bool isExisting=false; rwLock.AcquireWriterLock(lockTimeOut); try { if (!dictionary.ContainsKey(key)) dictionary.Add(key, value); else isExisting=true; } finally { rwLock.ReleaseWriterLock(); } if (isExisting) throw new IndexOutOfRangeException (); } /// <summary> /// 获取与指定的键相关联的值 /// Exceptions: /// ArgumentException - Dictionary 中已存在具有相同键的元素。 /// </summary> /// <param name="key">要添加的元素的键</param> /// <param name="value">当此方法返回时,如果找到该键,便会返回与指定的键相关 联的值; /// 否则,则会返回value 参数的类型默认值。该参数未经初始化即被传递</param> /// <returns>如果Dictionary 包含具有指定键的元素,则为true;否则为 false</returns> public bool TryGetValue(TKey key, out TValue value) { rwLock.AcquireReaderLock(lockTimeOut); bool result; try { result=dictionary.TryGetValue(key, out value); } finally { rwLock.ReleaseReaderLock(); } return result; } /// <summary> /// 从Dictionary 中移除所有的键和值 /// </summary> public void Clear() { if (dictionary.Count > 0) { rwLock.AcquireWriterLock(lockTimeOut); try { dictionary.Clear(); } finally { rwLock.ReleaseWriterLock(); } } } /// <summary> /// 确定Dictionary 是否包含指定的键 /// </summary> /// <param name="key">要在Dictionary 中定位的键</param> /// <returns>如果Dictionary 包含具有指定键的元素,则为true;否则为 false</returns> public bool ContainsKey(TKey key) { if (dictionary.Count <= 0) return false; bool result; rwLock.AcquireReaderLock(lockTimeOut); try { result=dictionary.ContainsKey(key); } finally { rwLock.ReleaseReaderLock(); } return result; } #endregion #region Properties /// <summary> /// 获取包含在Dictionary 中的键/值对的数目 /// </summary> public int Count { get { return dictionary.Count; } } #endregion } }
Unit Test
参考Common.Test项目的GenericCacheTest
2.3 准备一个集中访问配置文件的Broker
为了确保所有的模式示例均通过一个统一的出口访问配置文件,并向客户程序隔离具体配置文件的组成方式,增加一个ConfigurationBroker类来集中管理所有的配置对象实例。同时为了便于客户程序中无须使用System.Configuration,模式库会对每个配置节增加自己的配置对象接口,而不是基于System.Configiration自带的基类,并由其所在的配置节组通过实现IConfigurationSource接口,当ConfigurationBroker回调Load()方法的时候加载到ConfigurationBroker的配置对象缓冲里,如图2-2所示。
图2-2配置访问Broker结构
这里将IObjectBuilder的动态加载及配置作为验证ConfigurationBroker是否可以有效运行的单元测试。
示例代码如下:
C# ——IConfigurationSource
/// <summary> /// 定义每个配置节组的抽象动作 /// </summary> public interface IConfigurationSource { /// <summary> ///ConfigurationBroker 可以通过回调该方法,加载每个配置节组需要缓冲的配置对象 /// </summary> void Load(); }
C# ——ConfigurationBroker
using System; using System.Collections.Generic; using System.Configuration; namespace MarvellousWorks.PracticalPattern.Common { /// <summary> /// 集中的配置文件信息访问Broker /// </summary> public static class ConfigurationBroker { #region Fields /// <summary> /// 用于保存所有需要登记的通过配置获取的类型实体,使用线程安全的内存缓冲对象保存 /// </summary> private static readonly GenericCache<Type, object> cache; #endregion static ConfigurationBroker() { System.Configuration.Configuration config=ConfigurationManager. OpenExeConfiguration(ConfigurationUserLevel.None); cache=new GenericCache<Type, object>(); // 查找自定义的IConfigurationSource配置节组,并调用Load方法加载配置 缓冲对象 foreach (ConfigurationSectionGroup group in config.SectionGroups) if (typeof(IConfigurationSource). IsAssignableFrom(group.GetType())) ((IConfigurationSource)group).Load(); } /// <summary> /// 各配置项将客户程序需要使用的配置对象通过该方法缓存 /// </summary> /// <param name="type">配置对象的类型</param> /// <param name="item">实际的配置对象实例</param> public static void Add(Type type, object item) { if((type == null) || (item == null)) throw new NullReferenceException(); cache.Add(type, item); } public static void Add(KeyValuePair<Type, object> item){Add(item.Key, item.Value);} public static void Add(object item){Add(item.GetType(), item);} /// <summary> /// 获取缓冲的配置对象 /// </summary> /// <param name="type">配置对象的类型</param> /// <returns>实际的配置对象实例</returns> public static T GetConfigurationObject<T>() where T : class { if (cache.Count <= 0) return null; object result; if (!cache.TryGetValue(typeof(T), out result)) return null; else return (T)result; } } }
Unit Test
Common.Test项目的ConfigurationBrokerTest using MarvellousWorks.PracticalPattern.Common; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace MarvellousWorks.PracticalPattern.Common.Test { [TestClass()] public class ConfigurationBrokerTest { [TestMethod] public void Test() { IObjectBuilder builder = ConfigurationBroker.GetConfigurationObject<IObjectBuilder>(); Assert.IsNotNull(builder); } } }
2.4 Web?Not Web?
作为公共库的设计者,您的公司可能会根据市场需要开发出一套 Windows 版本、一套Web版本的应用或产品,相应地,您也可以通过编写两次代码分别提供一套不错的Windows版本库、一套Web版本库。
这里的Windows程序指Windows Application、Windows Control Library、Windows Service、Smart Client Application、Console Application、Office Project之类的传统上被称为“胖”客户端的应用;而Web应用指ASP.NET Web Application、ASP.NET Web Service Application、ASP.NET AJAX Application之类的“瘦”客户端应用。
您可能会问:“为什么呢?就连Enterprise Library的Common库都没有引用过System.Web或System.Windows这两个命名空间?只要不涉及具体应用类型的调用,我的公共库就不能够通用么?”
我的回答是:“视您的应用需要而定。”
如果要做到上下文和具体执行过程内容隔离,须要考虑如下两种模式:
● Windows程序中,如果上下文需要提供实际执行单元内部的共享,需要把它放置于每个线程内部。
●而对于Web应用,则需要置于更小的System.Web.HttpContext中。
那么,怎么做一个通用的Context类型,同时满足Windows应用和Web应用的需要?老办法:抽象+封装,前者需要抽象出一个类似IContext的接口,然后通过Factory根据上下文动态生成某一个特殊的上下文对象,这听起来就很别扭,不如采用后者,封装一个自适应的“通吃”的上下文对象——GenericContext,如表2-3所示。
表2-3
示例代码如下:
C#
using System; using System.Collections.Generic; using System.Web; namespace MarvellousWorks.PracticalPattern.Common { /// <summary> /// 通用的自定义上下文对象 /// </summary> public class GenericContext { /// <summary> /// 由于内部操作上,所有的容器类型均为Dictionary<string, object> /// 所以定义一个固定的类型名称 /// </summary> class NameBasedDictionary : Dictionary<string, object> { } /// <summary> /// 用于Windows 应用的线程级上下文成员容器 /// </summary> [ThreadStatic] private static NameBasedDictionary threadCache; /// <summary> /// 标识当前应用是否为Web 应用 /// </summary> private static readonly bool isWeb=CheckWhetherIsWeb(); /// <summary> /// Web 应用中Context 保存的键值 /// </summary> private const string ContextKey="marvellousWorks.context.web"; /// <summary> ///对于Web 应用,如果HttpContext对应内容元素没有初始化,则放置一个空容器 ///对于Windows 应用,由于threadCache为[ThreadStatic],无需该过程 /// </summary> public GenericContext() { if (isWeb && (HttpContext.Current.Items[ContextKey] == null)) HttpContext.Current.Items[ContextKey] = new NameBasedDictionary(); } /// <summary> /// 根据上下文成员名称,返回对应内容 /// <remarks> /// 由于threadCache或HttpContext 中的缓冲对象都会在构造过程中创建 // 因此,这里没有对cache == null的判断 /// </remarks> /// </summary> /// <param name="name">上下文成员键值。</param> /// <returns>对应内容</returns> public object this[string name] { get { if (string.IsNullOrEmpty(name)) return null; NameBasedDictionary cache=GetCache(); if (cache.Count <= 0) return null; object result; if (cache.TryGetValue(name, out result)) return result; else return null; } set { if (string.IsNullOrEmpty(name)) return; NameBasedDictionary cache=GetCache(); object temp; if (cache.TryGetValue(name, out temp)) cache[name]=value; else cache.Add(name, value); } } /// <summary> /// 根据应用类型获取相应上下文缓冲对象 /// </summary> /// <returns>缓冲对象</returns> private static NameBasedDictionary GetCache() { NameBasedDictionary cache; if (isWeb) cache=(NameBasedDictionary)HttpContext. Current.Items[ContextKey]; else cache=threadCache; if (cache == null) cache=new NameBasedDictionary(); if (isWeb) HttpContext.Current.Items[ContextKey]=cache; else threadCache=cache; return cache; } /// <summary> /// 判断当前应用是否为Web 应用的Helper方法(非官方方法) /// </summary> /// <returns></returns> private static bool CheckWhetherIsWeb() { bool result=false; AppDomain domain=AppDomain.CurrentDomain; try { if (domain.ShadowCopyFiles) result=(HttpContext.Current.GetType() != null); } catch (System.Exception){} return result; } } }
Unit Test (Web)
参考Common.Test.Web项目的GenericContextTest.aspx和GenericContextTest.cs namespace Common.Test.Web { public partial class GenericContextTest : System.Web.UI.Page { private const string Key="id"; private GenericContext context=new GenericContext(); protected void Page_Load(object sender, EventArgs e) { context[Key]=Guid.NewGuid().ToString(); step1.Text=context[Key] as string; step2.Text=context[Key] as string; step3.Text=context[Key] as string; } } }
测试结果: 贯穿在整个HttpContext中,每个Step都可以直接使用同一个GenericContext。
Unit Test (Windows Multi-Threading)
参考Common.Test.Windows项目的GenericContextTest.cs using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.Threading; using MarvellousWorks.PracticalPattern.Common; namespace Common.Test.Windows { [TestClass()] public class GenericContextTest { class WorkItem { private GenericContext context; private const string Key="id"; private static IList<string> works=new List<string>(); public string Id { get { return (string)context[Key]; } } public void Start() { context=new GenericContext(); context[Key]=Guid.NewGuid().ToString(); works.Add(Id); } public static IList<string> Works { get { return works; } } } [TestMethod] public void Test() { Thread[] threads=new Thread[3]; for(int i=0; i<threads.Length; i++) { threads[i]=new Thread(new ThreadStart(new WorkItem().Start)); threads[i].Start(); } Thread.Sleep(1000); Assert.AreNotEqual<string>(WorkItem.Works[0], WorkItem.Works[1]); Assert.AreNotEqual<string>(WorkItem.Works[1], WorkItem.Works[2]); Assert.AreNotEqual<string>(WorkItem.Works[2], WorkItem.Works[0]); } } } 测试结果: 通过定义[ThreadStatic],可以保证在Multi-Threading环境下每个线程使用独立的上下文对象。
2.5 小结
至此,本章已经为后面具体工程化实现每个设计模式准备了如下基础内容:
● 一个替代new()创建对象实例的IObjectBuilder。
● 一个用于保存和管理集合类型的、线程安全的、轻量的泛型内存缓冲类型。
● 一个访问配置文件的ConfigurationBroker。
● 一个轻量的Web和Windows应用自适应的上下文对象。
● 可以结合System.MulticastDeleagate管理一组委托对象。
这些已经为后面的实现准备了基础工具,下文正式进入设计模式实现的介绍。
Are you Ready?Let's go。