第1篇 预备知识
发掘用C#语言进行面向对象化设计的潜力
第1篇主要是对本书进行一些概括性介绍,包括两个部分:
■ 重新研读C#语言
虽然本书不是一本介绍C#语言的书,但是由于在工程中,特别是规模相对较大的工程中常常需要使用一些C#语言特有的高级特性,这些特性的介绍又要结合面向对象开发基础之上扩展的一些诸如“异步通知机制”、“配置——对象映射机制”、“操作符重载”等特性,所以专门增加了一章,介绍C#语言这类开发语言的梗概内容。
■ 开始每个设计模式之前
为了切合本书的副标题,如我们开发一般项目的习惯一样,这一章主要准备一些后面每个模式具体编码中需要使用的公共机制的实现。
第1章 重新研读C#语言
1.1 说明
1.2 C#部分语法内容扩展
1.3 可重载运算符(Overloadable Operators)与转换运算符(Conversion Operators)
1.4 面向插件架构和现场部署的配置系统设计
1.5实现依赖注入
1.1 说明
本章以工程化使用为目的,对C#和.NET Framework提供的几个平时开发时不引人注意的特征进行介绍,它们对于提高代码扩展性、灵活性很关键,务求用“很 C#”的方式解决以往设计模式Example之外必须面临的一些问题。它们主要包括:
● Namespace(命名空间);
● Delegate(委托);
● Generics(泛型);
● Attribute(属性);
● Indexer(索引器);
● Iterator(迭代器);
● Overloadable Operators(可重载运算符)与Conversion Operators(转换运算符);
● Configuration(对象化配置访问);
由于降低类间耦合关系一直是设计上控制变化范围的常用手段之一,所以在设计模式之外补充有关用C#实现“依赖注入”(Dependency Injection)的方法。
有关设计模式在Threading(多线程)模型下实现需注意的内容,笔者将在相关章节的工程化分析部分介绍。
1.2 C#部分语法内容扩展
1.2.1 命名空间(Namespace)
尴尬的现实状况
是否有很好的命名空间规划是工程化代码与非工程化代码一个很明显的区别。
尤其对于大型的组织而言,如果涉及的产品线、项目、公共平台很多,如何通过命名空间把所有的代码资源有效地组织起来,恐怕是实施项目前要考虑的主要问题。作为一个树形体系,最好有组织级统一的分类标准,目的很明确——用的时候能很容易找到。
做到这点并不容易,原因如下:
● 习惯中很难改变的缩写命名:CAD 很容易在声明的时候被命名为.***CAD,但事实上Design Guideline 建议的是Cad,这样使用者和定义者之间就存在一些小小的错位。在99%的情况下这个问题不会发生,因为您只要一个点,IntelliSense就帮您列出来了,另外的1%则发生在后绑定调用的情况下。
● 不统一的命名:涉及加密的库,可能 A 被定义为Encrypt,B 被定义为Crypto 或Cryptography。来自匈牙利命名法的“遗毒”也常常成为新旧开发人员统一命名的障碍,而且会直接影响到命名空间的定义。
● 组织或您上司的认同:技术总监不认为命名规范是他需要关心的事情,下面的架构师更关心的是结构,再下面的项目经理关注的是资源调度和进度,具体的实施人员恐怕没有多少机会规定其他同事该怎么命名,那么谁来关心命名空间呢?
企业.NET类型系统的命名空间规划示例
无论如何,即便没有办法在组织级统一命名空间,为了您所带领的团队现在做的工作在以后能更容易地被应用,或者仅仅为你自己的职业生涯好好“储蓄”,在动笔编写第一行程序之前,先规划好命名空间吧。
可参考的建议来自Design Guideline,示例代码如下:
Txt
<Company>.(<Product>|<Technology>)[.<Feature>][.<Subnamespace>] 例如:Microsoft.WindowsMobile.DirectX.
本书选择了MarvellousWorks.PracticalPattern 作为根命名空间,但前提是假设这个公司很小,相关的项目(或产品)很少,而且没有多少组织级通用的代码资源。如果假设它为一个大型软件企业,套用笔者自己组织的命名空间,总体命名空间的规划情况如下:
C#(一级命名空间)
namespace MarvellousWorks.Application namespace MarvellousWorks.Foundation namespace MarvellousWorks.Framework namespace MarvellousWorks.Utility namespace MarvellousWorks.Training
● Application:代表项目或产品。
● Foundation:代表公共库,类似Enterprise Library之类的公共基础库、基于企业设备和操作系统平台的通用的图形处理引擎等,但都是纯粹的Class Library,没有UI元素。
● Framework:组织通用的框架,基于 Foundation之上,面向某个开发领域扩充的Class Library和控件,其本身不能独立运行,但完全可以集成在具体的项目或产品中,比如通用的授权框架、完全Ajax化的前后端组件、报表和打印中间件。
● Utility:企业内部各种工具,比如现场故障排查工具、Dump和日志分析工具。
● Training:完全面向培训用途,是对企业自身 Application、Foundation、Framework(甚至Utility)使用的Examples。
几个一级命名空间的布局如图1-1所示。
图1-1 一个建议的一级命名空间布局和调用关系
本次的PracticalPattern作为实际工程化代码,不属于MarvellousWorks.Training,而应划入 MarvellousWorks.Foundation,并将它作为最核心的算法框架,放入 MarvellousWorks. Foundation.Core.PracticalPattern。
小结
不论您最终如何定义命名空间,其实它体现的是您意志中对代码资源的规划,如果您觉得工程化的设计模式不是您希望开发团队要考虑的内容,没关系,您可以忽略这部分内容。
这样MarvellousWorks.Foundation.Accessories.PracticalPattern
这样MarvellousWorks.Foundation.Core.Accessories.PracticalPattern
或者这样TestSolution. PracticalPattern
……
1.2.2 简洁的异步通知机制——委托(Delegate)
用Delegate完成异步调用
Delegate可用于对方法的引用,它可以作为方法的参数出现,也可以作为类的成员出现,尤其在异步调用中,Delegate可以定义回调目标方法,示例代码如下:
C#
using System; using System.Collections.Generic; using System.Threading; namespace MarvellousWorks.PracticalPattern.Concept.Delegating { public class AsyncInvoker { // 记录异步执行的结果 private IList<string> output=new List<string>(); public AsyncInvoker() { Timer slowTimer=new Timer(new TimerCallback(OnTimerInterval), "slow", 2500, 2500); Timer fastTimer=new Timer(new TimerCallback(OnTimerInterval), "fast", 2000, 2000); output.Add("method"); } private void OnTimerInterval(object state) { output.Add(state as string); } public IList<string> Output { get { return output; } } } }
Unit Test
[TestMethod()] public void Test() { AsyncInvoker asyncInvoker=new AsyncInvoker(); System.Threading.Thread.Sleep(3000); Assert.AreEqual<string>("method", asyncInvoker.Output[0]); Assert.AreEqual<string>("fast", asyncInvoker.Output[1]); Assert.AreEqual<string>("slow", asyncInvoker.Output[2]); }
在上面的例子中,TimerCallback就是一个Delegate,它定义了每个Timer触发时需要回调的方法。由于它与主流程间是异步执行的,因此从测试结果看,主流程首先执行完成,而两个快慢 Timer 则先后执行。除此之外,上述事例还表达了一个非常重要的意图:Delegate是对具体方法的抽象,它屏蔽了Delegate的调用者与实际执行方法间的关联关系。例如上例中调用者是Timer,而执行方法是某个AsyncInvoker实例的OnTimerInterval方法。很多行为型模式可以采用Delegate这种抽象的操作方法表示。
对n的通知
进一步,通过Delegate 集合可以实现一个对象与多个抽象方法的1∶1∶n(调用者∶Delegate集合∶抽象方法)的关系,如图1-2所示。
图1-2 用Delegate集合实现客户程序与多个抽象方法调用的关联
其示例代码如下:
C#
using System; using System.Collections.Generic; namespace MarvellousWorks.PracticalPattern.Concept.Delegating { public delegate void StringAssignmentEventHandler(); //抽象的操作方法 public class InvokeList { private IList<StringAssignmentEventHandler> handlers; private string[] message=new string[3]; public InvokeList() { // 绑定一组抽象方法 handlers=new List<StringAssignmentEventHandler>(); handlers.Add(AppendHello); handlers.Add(AppendComma); handlers.Add(AppendWorld); } public void Invoke() { foreach (StringAssignmentEventHandler handler in handlers) handler(); } public string this[int index] { get { return message[index]; } } // 具体操作方法 public void AppendHello() { message[0]="hello"; } public void AppendComma() { message[1]=","; } public void AppendWorld() { message[2]="world"; } } }
Unit Test
[TestMethod()] public void Test() { string message=string.Empty; InvokeList list=new InvokeList(); list.Invoke(); Assert.AreEqual<string>("hello,world",list[0]+list[1]+list[2]); }
不过,上面的实现方式并不是“原汁原味”的.NET做法,因为delegate声明的Delegate类型其实本身继承自System.MulticastDelegate,从名字上不难发现它表示广播(见图1-3),也就是它的调用列表中可以拥有多个委托,同时它重载了“+=”和“-=”运算符以便于使用,所以更为简单的写法如下:
C#
public MulticastDelegateInvoker() { StringAssignmentEventHandler handler=null; handler += new StringAssignmentEventHandler(AppendHello); handler += new StringAssignmentEventHandler(AppendComma); handler += new StringAssignmentEventHandler(AppendWorld); handler.Invoke(); }
IL (StringAssignmentEventHandler)
.class public auto ansi sealed StringAssignmentEventHandler extends [mscorlib]System.MulticastDelegate{}
图1-3 基于MulticastDelegate的广播结构
调用匿名方法
方法,也就是一小段可以重用的处理过程,一般我们都会把它独立编写出来,但如果这个逻辑本身非常简单,而且其变化范围仅仅是一个非常小的局部,或者如上面两个例子那样需要把抽象的处理作为一个参数传递给其他逻辑调用,那么匿名方法(Anonymous Method)是个很不错的选择。此外,匿名方法结合反射会大大提升公共库的灵活性。
从工程角度看,匿名方法无论在创建型模式、行为型模式还是结构型模式的实现中都大有用处,另外在架构模式中(包括Pipeline、Page Controller、Gateway等),匿名方法非常适合数量较大,但逻辑控制很短的调用。
笔者喜欢匿名方法的主要原因是用它写的代码看起来比较简洁,上例构造函数用匿名方法稍作修改后如下所示:
C#
public InvokeList() { StringAssignmentEventHandler handler=null; handler += delegate { message[0]="hello"; }; handler += delegate { message[1]=","; }; handler += delegate { message[2]="world"; }; handler.Invoke(); }
调用重载方法
上例中StringAssignmentEventHandler 定义了参数为空的方法,虽然在实际工程中最初定义模式时可以严格遵守一个特定的Delegate描述,但随着软件的升级,难免会要求新的定义,最明显的就是需要调用的方法有新的重载。虽然在经典的设计模式介绍中会列举出关键的方法名称,但似乎从未提示过如何处理重载的情况(如Observer的Notify()方法)。为此,在实际工程中,笔者会采用以下几个办法:
● 声明Delegate参数为params object[] parameters,这种方法虽然特别通用,甚至可以称之为Smart Delegate,但怎么看都觉得别扭,原因是重载的方法参数类型不一定相同(值型、引用型都可能存在),因此只好不用强类型,也无法使用泛型。同时把params object[] parameters放到目标方法上,目标方法内部再根据参数信息自己选择该调用哪个方法反馈的Delegate。这时整个系统内部仅须声明一个参数为params object[] parameters的“万金油”Delegate也就够了,但这样会让每个目标类的实现非常Ugly,如图1-4所示。
图1-4 不怎么中看的智能委托实现
UML中没有所谓的“方法图”,但为了表示逻辑组成的关系,笔者对方法采用了类似类图的表示。这里Delegate为纯虚方法定义,而具体目标方法被视为其实现类,对方法而言相应的属性代表方法的参数。
● 其次就是类似ObjectBuilder那种很重磅的实现方式,具体调用哪个目标方法全部由配置系统决定。
● 一般情况下,笔者喜欢用抽象类System.Delegate完成。例如图1-5中C1、C2、C3虽然分别对方法A、S、M都有重载,实际操作上下文仅须使用三个参数的重载方法,通过组合System.Delegate就可以实现该调用要求。
图1-5 改进的智能委托
UglySmartDelegateInvoker.cs和UglySmartDelegateInvokerTest.cs的示例代码如下:
C#
using System; using System.Collections.Generic; namespace MarvellousWorks.PracticalPattern.Concept.Delegating { public delegate void MemoHandler(int x, int y, IDictionary<string, int> data); //对具有重载的多个目标方法Delegate public class OverloadableDelegateInvoker { private MemoHandler handler; public OverloadableDelegateInvoker() { Type type=typeof(MemoHandler); Delegate d=Delegate.CreateDelegate(type, new C1(), "A"); d=Delegate.Combine(d, Delegate.CreateDelegate(type, new C2(),"S")); d=Delegate.Combine(d, Delegate.CreateDelegate(type, new C3(),"M")); handler=(MemoHandler)d; } public void Memo(int x, int y, IDictionary<string, int> data) { handler(x, y, data); } } }
Unit Test
[TestMethod] public void Test() { int result=10; int expected=result; OverloadableDelegateInvoker invoker = new OverloadableDelegateInvoker(); IDictionary<string, int> data=new Dictionary<string, int>(); invoker.Memo(1, 2, data); Assert.AreEqual<int>(1+2, data["A"]); Assert.AreEqual<int>(1 - 2, data["S"]); Assert.AreEqual<int>(1 * 2, data["M"]); }
小结
由于大量设计模式的书籍都采用了Java语言,即便冠名为C#,从编码行文看实际上也采用了类Java的描述,相关的设计模式更倾向于用Interface而非Delegate,因此在实际的.NET工程中存在如下不便的地方:
● 如果将异步调用任务交给统一的线程池机制来维护,则需要增加额外开发量,不如直接使用.NET Framework包装好的如TimerCallback之类的Delegate。
● C#的Delegate自身就具有组合特征,可以通过组合加组播的方式,自动代理客户程序对多个目标方法的抽象调用。
● Interface可以使用的抽象方法描述是固定的,而使用Delegate,在运行过程中,可以根据动态要求自动适配目标方法。
1.2.3 考验你的算法抽象能力——泛型(Generics)
抽象及抽象能力支持
自第一次使用STL之后,笔者一直特别喜欢具有模板类的语言,主要原因有三个:
● 算法更抽象。
● 嵌套之后(例如C<T<TKey, TValue>, V>),可以表示抽象之上的进一步抽象。
● 强参数类型无论在开发过程、编译过程、运行过程中都有优势。
除此之外,还有个人感觉上的原因——看Generics的代码很酷。
先谈抽象,也许已经有太多的技术资料讨论如何Care业务,没错,那始终是第一位的,不过作为开发人员,还要更多地顾及自己那始终在抚摸键盘的手指,以及办公桌上的时钟,如果一项任务能通过抽象这个途径减少一些开发量,何乐而不为呢?如果您是组织内部的平台组成员,负责公共开发平台建设或通用行业中间件设计,那么抽象就不是optional而是essential的事情了,原因很简单——您大概只能知道别人会怎么重用您的工作,大概知道他们要什么样的内容,但您不能替他们做完一切。
那么,C#提供给了我们什么样的抽象能力支持呢?
● Class:提供了对现实世界的抽象。
● Interface:提供了对Class行为的抽象。
● Delegate:对方法的抽象。
● Attribute:对类型元数据的抽象。
● Generics:给上述因素进一步、进两步、直至进n步抽象的机会。
应用于模式
首先,各种创建型模式可以从中受益。Factory 可以加工出抽象类型来——Interface、Abstract Class,甚至在某个语境层面的父类Class,但Factory自身的写法非常一致,如果为接口1写一个、接口2写一个……接口100也写一个,内心应该是个冲击——“我,我……我写了重复的代码”,修改的时候也一样。当然,如果您的老板是按照代码行数计算绩效或发薪的话,另当别论。在不使用配置系统的情况下,先考虑用typeName对具有无参构造函数的类提供一个相对通用的Factory,这里的typeName采用的是可以被System.Type类识别的名称,并由System.Activator动态生成,例如:
● TopNamespace.SubNameSpace.ContainingClass, MyAssembly
● ContainingClass, MyAssembly, Version=1.3.0.0, PublicKeyToken=b17a5c561934e089
示例代码如下:
C#
using System; namespace MarvellousWorks.PracticalPattern.Concept.Generics { public static class RawGenericFactory { public static T Create<T>(string typeName) { return (T)Activator.CreateInstance(Type.GetType(typeName)); } } }
Unit Test
[TestClass()] public class RawGenericFactoryTest { interface IProduct { } class ConcreteProduct : IProduct { } [TestMethod] public void Test() { string typeName=typeof(ConcreteProduct).AssemblyQualifiedName; IProduct product=RawGenericFactory.Create<IProduct>(typeName); Assert.IsNotNull(product); Assert.AreEqual<string>(typeName, product.GetType().AssemblyQualifiedName); } }
或者可以把Factory按照以往的方式,设计为非静态类。示例代码如下:
C#
public class RawGenericFactory<T> { public T Create(string typeName) { return (T)Activator.CreateInstance(Type.GetType(typeName)); } }
Unit Test
IProduct product=new RawGenericFactory<IProduct>().Create(typeName);
类似的功能可以通过通用的System.Object类完成,但通过类型转换前置则是在保证Factory 通用性的前提下,与客户程序间仅交付“规约”的产品——抽象类型的本意。相对而言,毕竟使用Factory的客户代码相对定义Factory自身的代码出现频率要高一些,把须要多次执行的类型转换成执行一次,下家使用的时候多少也轻松一点(或仅是一点点)。在实际工程中,typeName 一般都是根据调用上下文转译过来的,工厂方法一般借助配置访问部分获得实际需要生产的目标类型。
如果这个构造过程比较复杂,还要考虑用Builder模式来装配,经典的Builder模式使用通常的C#实现方式,如图1-6所示。
图1-6 经典创建者模式的静态结构
但实际工程中除了BuildPartA()、BuildPartB()之外,还需要增加下面几阶段处理,比如:
● Pre Constructor:整个BuildUp过程之前,获得每个Part的配置信息和配置中有关如何把Part装配为IProduct的说明信息,尤其是BuildPart的次序。
● Initialization:完成每个Part之后的装配过程。
● Post Initialization:装配之后是否需要考虑把实例池化、是否需要捆绑必要的监控属性(Instrument Attribute)、是否需要生成日志或做好串行化准备等事情。
如果把它们全部交给Director完成,那么Director就太辛苦了,不妨把每个阶段抽象为特殊的接口,每个阶段自身又可以由很多的抽象步骤完成,这样,Director还是做“主持大局”的工作,而无须事事“亲历亲为”,保证Builder框架的相对稳定,如图1-7所示。它们将结合配置访问在Builder模式部分详细介绍其内容。
图1-7 改进后的创建者模式的静态结构
这样,DirectorBase仅须保存两个集合——Builders和BuildStrategies。其中Builders是IBuilder的集合,而BuildStrategies则提供了分类保存IBuildPhase(及其相关IBuildStep)的功能。那么使用Generics与全部保存为System.Object有多少区别呢?
● 便于阅读,需要什么类型的抽象类型(接口或抽象类),一目了然。
● 编译的时候就进行强类型检查,而不是在运行过程中才获得一个InvalidCastException。
● Build负责装备需要的类型,是应用中相对最繁忙的类型之一,如上Build涉及多个Build Phase,每个Build Phase又可能包括多个Build Step,即便做System.Object与具体接口的类型转换只需要增加很小的负载,但如果用于比较繁忙的业务情景,就不再会是个小负载了。
Generics 在结构型和行为型模式中的作用主要还是提高算法的适用性,例如:Adapter完成的是接口的转换,从更抽象的角度看,就是一个Type Cast(类型转换),所以用泛型可以定义出一个普适的IGenericAdapter,示例代码如下:
C#
using System; namespace MarvellousWorks.PracticalPattern.Concept.Generics { public interface ITarget { void Request();} public interface IAdaptee { void SpecifiedRequest();} public abstract class GenericAdapterBase<T> : ITarget where T : IAdaptee, new() { public virtual void Request() { new T().SpecifiedRequest(); } } }
容器
泛型的另一个主要用途是建立各种强类型的集合类型,也就是定义容器中具体内容的类型。设计模式涉及管理“一组”对象的不是少数,仅最经典的描述中就包括了:
● Builder的Director;
● Composite;
● Flyweight自己的Factory;
● Command的Invoker;
● Memento的Care Keeper;
● State和Strategy的Context。
除此之外,实际工程中还要考虑容器的访问方式,即需要IDictionary<K, V>、IList<T>、Stack<T>和Queue<T>之类的类型。如果不使用泛型,那么后面的应用就纯属于“碰运气”访问,因为无法确定别人会怎么用你的公共库,至于到底外面传入的是什么?不清楚,都被容器包成为System.Object 了,您可以假设它是实现了某个接口的类实例。但如果一个企业的公共代码库处处都基于“估计”,这个产品就太不靠谱了。相对一般的泛型类而言,泛型对集合类意义更重要:接口和参数更明确,而且不仅仅停留在UML的图纸上。
类型参数约束
另一个更为清晰的定义手段来自类型参数的约束机制,除了MSDN 介绍的控制实例化过程外,还有如下注意事项:
● 当类成员使用相同类型参数的同时,该类型参数的约束也同样适用于相关成员。示例代码如下:
C#
namespace MarvellousWorks.PracticalPattern.Concept.Generics { public interface IOrganization { } public abstract class UserBase<TKey, TOrganization> where TOrganization : class, IOrganization, new() { public abstract TOrganization Organization { get;} // method public abstract void Promotion(TOrganization newOrganization); // property delegate TOrganization OrganizationChangedHandler(); // delegate } }
● 参数约束不适用于Attribute。
类型参数和类型参数约束的搭配让您的设计“收放自如”:类型参数通过抽象具体操作类型,让类和接口更加通用——“放”;但是您的算法又必须“有的放矢”,适用于特定的类型,并且当别人使用这些算法的时候,能通过一个“点”,即可由IntelliSense反射出一些东西,让编码更为快捷,这就需要对类型参数做限制——“收”。
本书中,类型参数约束的使用会更加频繁,作为Example没关系,无参的构造函数、无参的方法、还有void的返回值就可以了,只要能Console.WriteLine;但是作为工程化代码,没有 where T : IProduct, new()的类型约束,在Factory里面new T()的时候就会提示编译错误,更不会返回实例结果。所以,如果您不是自己练手,而是真的要把设计模式应用于具体工程,笔者有两个建议:
1. 设计每一个模式角色类的时候,要根据客户程序的需要,反复斟酌类型参数约束。
2. 除了容器类以外,尽可能不要在生产代码里出现无约束的类型参数(“裸类型约束”)。
除此之外,如果您把设计模式应用到组织的公共开发库,可能还有如“where T : A || B”或“where T : A && B”之类的代码,也就是说,您希望某个特定算法仅仅被某几个特定抽象类型使用。很可惜,.NET 不支持。那么变通的办法和我们用设计模式思想解耦其他关联的办法一样——引入新对象。示例代码如下:
C#
namespace MarvellousWorks.PracticalPattern.Concept.Generics { // 解决类似where T : A || B的需要 interface INewComer { } class OrA : INewComer { } class OrB : INewComer { } class OrClient<T> where T : INewComer { } // 解决类似where T : A && B的需要 interface Layer11 { } interface Layer12 { } interface Layer2 : Layer11, Layer12 { } class AndA : Layer11 { } class AndB : Layer12 { } class AndClient<T> where T : Layer12 { } }
不过重复写出INewComer、ILayer比较费时费神,可以用Visual Studio .NET的“Extract Interface”菜单快速解决这个问题,如图1-8所示。
图1-8 使用Visual Studio .NET菜单简化代码框架的生成
小结
如果您的代码将被反复重用,只要进度允许,尽量泛型吧,因为:
● 省去Cast过来Cast过去后,效率提高了。
●抽象给您的代码带来更多的适应性。
● 减少接口和参数的歧义。
1.2.4 用作标签的方式扩展对象特性——属性(Attribute)
从使用者角度看,恐怕没有比Attribute更方便的了,基于属性的编码完全站在实际逻辑外面,如果说经典设计模式中Decorator 通过套接在不生成子类的情况下为类添加职责,那么Attribute则通过一个更简洁的方法为类“装饰”出职责和特性的机制。
用Attribute指导模式
这里以Builder模式为例。首先,按照经典的做法定义抽象Builder和一个实体 Builder,在此为了后续Unit Test的方便,增加了一个Log属性,记录每个Build Part的调用过程。代码如下:
C#
// Builder抽象行为定义 public interface IAttributedBuilder { IList<string> Log { get;} // 记录Builder的执行情况 void BuildPartA(); void BuildPartB(); void BuildPartC(); } public class AttributedBuilder : IAttributedBuilder { private IList<string> log=new List<string>(); public IList<string> Log { get { return log; } } public void BuildPartA() { log.Add("a"); } public void BuildPartB() { log.Add("b"); } public void BuildPartC() { log.Add("c"); } }
此后,将Director 指导 Builder 组装的每个步骤通过DirectorAttribute属性类来表示,而Director在BuildUp的过程中,通过反射获得相关Builder的DirectorAttribute列表,对列表进行优先级排序后,执行每个DirectorAttribute指向的Build Part方法。代码如下:
C#
// 通过attribute 扩展Director [AttributeUsage(AttributeTargets.Class, AllowMultiple =true)] public sealed class DirectorAttribute : Attribute, IComparable<DirectorAttribute> { private int priority; // 执行优先级 private string method; public DirectorAttribute(int priority, string method) { this.priority=priority; this.method=method; } public int Priority { get { return this.priority; } } public string Method { get { return this.method; } } // 提供按照优先级比较的ICompare<T>实现, 由于Array.Sort<T> //实际是升序排列,而Array.Reverse 是完全反转,因此这里调整了 // 比较的方式为“输入参数优先级-当前实例优先级” public int CompareTo(DirectorAttribute attribute) { return attribute.priority - this.priority; } } public class Director { public void BuildUp(IAttributedBuilder builder) { // 获取Builder的DirectorAttribute属性 object[] attributes=builder.GetType().GetCustomAttributes( typeof(DirectorAttribute), false); if (attributes.Length <= 0) return; DirectorAttribute[] directors = new DirectorAttribute[attributes.Length]; for (int i=0; i < attributes.Length; i++) directors[i]=(DirectorAttribute)attributes[i]; // 按每个DirectorAttribute 优先级逆序排序后,逐个执行 Array.Sort<DirectorAttribute>(directors); foreach (DirectorAttribute attribute in directors) InvokeBuildPartMethod(builder, attribute); } // helper method : 按照DirectorAttribute的要求,执行相关的Builder方法 private void InvokeBuildPartMethod( IAttributedBuilder builder, DirectorAttribute attribute) { switch (attribute.Method) { case "BuildPartA": builder.BuildPartA(); break; case "BuildPartB": builder.BuildPartB(); break; case "BuildPartC": builder.BuildPartC(); break; } } }
接着,用做好的DirectorAttribute来定义Builder的装配过程。示例代码如下:
C#
[Director(1, "BuildPartB")] [Director(2, "BuildPartA")] public class AttributedBuilder : IAttributedBuilder
Unit Test
[TestMethod] public void Test() { IAttributedBuilder builder=new AttributedBuilder(); Director director=new Director(); director.BuildUp(builder); Assert.AreEqual<string>("a", builder.Log[0]); Assert.AreEqual<string>("b", builder.Log[1]); }
如果要修改Builder的装配过程,仅需要增加、维护相关属性即可,例如下列代码:
C#
[Director(3, "BuildPartA")] [Director(2, "BuildPartB")] [Director(1, "BuildPartC")] public class AttributedBuilder : IAttributedBuilder
Unit Test
[TestMethod] public void Test() { IAttributedBuilder builder=new AttributedBuilder(); Director director=new Director(); director.BuildUp(builder); Assert.AreEqual<string>("a", builder.Log[0]); Assert.AreEqual<string>("b", builder.Log[1]); Assert.AreEqual<string>("c", builder.Log[2]); }
实际工程中,Attribute 常常会和反射、配置一同使用,比如[Director(2, "BuildPartA")]中优先级和方法名称都可以在配置文件定义。虽然看起来开发阶段增加了一些额外的代码工作(例如Director和DirectorAttribute的编码),但从使用者角度看,减少了反复定义Director相关BuildUp装配的过程。对于其他行为型和结构型模式,Attribute同样可以从很多方面扩展,减少客户程序使用的操作复杂程度。比如:
● 把State声明为属性。
● 把各类Proxy需要的控制属性通过Attribute体现出来。
进一步方便客户程序调用
虽然可通过Attribute扩展模式中相关角色类的特征,但如果BuildUp的不是三个Part,而是一架飞机,罗列40多个DirectorAttribute就似乎不太合适,代码看起来也太“涩”了;另一个问题,动态地增加 DirectorAttribute需要反复地编译代码,把本来的一点点方便都掩盖了。
解决方法:让Attribute具有组合特性,把配置拉进来。这样做带来的变化就是可以改配置。
参考上面Delegate部分的MulticastDelegate,可以扩展出具有Multicast特性的Attribute,它采用经典的Composite来完成,如图1-9所示。
这样,每个IAttributedBuilder关联的仅仅是一个IDirectorAttribute,它可能是一个原子的DirectorAttribute,也可能是代表了一组DirectorAttributeBase的MulticastDirectorAttribute,甚至一个复杂的DirectorAttribute 树。即便相对比较复杂也没有关系,毕竟我们的配置文件本身就是XML的,它就是棵树。这样,如果IAttributedBuilder的属性需要修改,比如增加或删除一个DirectorAttribute,修改配置文件即可,始终可以保持 IAttributedBuilder与IDirectorAttribute的1∶1。
图1-9 具有组合关系和Attribute构造器结构
小结
也许更多的时候,我们感觉基于Attribute的开发仅仅是新瓶装旧酒,OO本质没有什么变化,但在工程上,它是个“让利”给下家的途径——用着更方便、写着更简单。
1.2.5 用索引器简化的C#类型信息访问
索引器的确是很精致的语法元素。
服务于集合
索引器的出现,让我们首次感觉到真正意义上的容器类型或集合类型的存在。在它出现之前,要达到类似的效果,主要有两种选择:
1. 保存为数组,然后按照for(int i=0; i<arr.Length; i++)读取。
2. 提供一组get***(idx)和put/set***(idx)的方法。
虽然Java借鉴C#已经改进了很多,但是在Indexer上没有变化,相应的集合类型都是通过add、remove、get之类的方法读取。C#中的Indexer给人一种更“透彻”的感觉,集合类型就是集合类型,有自己专用但又最简洁的访问方式,而且同一种类型可以有不同的索引访问方式。
企业应用中存在非常多的编码:经营单位编码、货币类型编码、工作流步骤编码等,应用中常常把它们作为参数使用。回想一下我们在建立这些表的时候仅仅有一个PK(Primary Key),还是常常在PK之外,增加一些 IDX(索引)呢?可能就一个PK,也可能是PK +IList<IDX>;Indexer也一样,可以有一个,也可以有多个。
封装单列集合的访问
即便是单列集合,其应用也非常普遍,以MSDN的Index Tab为例,输入Array之后的效果如图1-10所示。
图1-10 MSDN Library的索引查询结果
也就是说,索引不仅仅是我们常规思维中1:1的精确匹配,应用中尤其涉及和用户交互的过程,即便是单列数据也常常会在Index时出现1:n的情况。示例代码如下:
C#
using System; namespace MarvellousWorks.PracticalPattern.Concept.Indexer { public class SingleColumnCollection { private static string[] countries=new string[] { "china", "chile", "uk" }; public string this[int index] { get { return countries[index]; } } public string[] this[string name] { get { if((countries == null) || (countries.Length <= 0)) return null; return Array.FindAll<string>(countries, delegate(string candicate) { return candicate.StartsWith(name); }); } } } }
Unit Test
using Microsoft.VisualStudio.TestTools.UnitTesting; using MarvellousWorks.PracticalPattern.Concept.Indexer; namespace MarvellousWorks.PracticalPattern.Concept.Test { [TestClass()] public class SimpleColumnCollectionTest { [TestMethod] public void Test() { SingleColumnCollection c=new SingleColumnCollection(); Assert.AreEqual<string>("china", c[0]); Assert.AreEqual<int>(2, c["ch"].Length); // 命中china和chile两项 Assert.AreEqual<string>("china", c["ch"][0]); } } }
当然,应用中还存在另一种解决办法——“长长的流水账”方式,WinForm方式还好,但如果是网页(见图1-11)——“反正我把知道的都告诉你了,耐心等待,然后自己选吧;至于还想看到里面的方法,算了吧,那么多内容一次都告诉你我嘴皮子都得磨破了。”
图1-11 JavaDoc的索引结果
多列的集合
多列的集合平时用得最多的恐怕非System.Data下的DataSet和DataTable莫属了,我们看看Indexer “over Indexer 再 over Indexer”之后的效果,示例代码如下:
C#
using System; using System.Data; namespace MarvellousWorks.PracticalPattern.Concept.Indexer { public class MultiColumnCollection { private static DataSet data=new DataSet(); static MultiColumnCollection() { data.Tables.Add("Data"); data.Tables[0].Columns.Add("name"); data.Tables[0].Columns.Add("gender"); data.Tables[0].Rows.Add(new string[] { "joe", "male" }); data.Tables[0].Rows.Add(new string[] { "alice", "female" }); } public static DataSet Data { get { return data; } } } }
Unit Test
[TestClass()] public class MultiColumnCollectionTest { [TestMethod] public void Test() { Assert.AreEqual("joe", MultiColumnCollection.Data.Tables[0].Rows[0]["name"]); Assert.AreEqual("female", MultiColumnCollection.Data.Tables[0].Rows[1][1]); } }
实现类似RDBMS中联合主键或唯一性索引的访问
“索引器”这个名称使我们很自然地联想到RDBMS(关系数据库)中的索引,就如我们在设计数据库逻辑结构的过程一样,为了唯一标注每条记录,常常会用到主键或唯一性索引,而构成它们属性(列)的可能是一项也可能是几项的联合。.NET 平台为了跨层调用的方便,从一开始就支持离线的DataSet和基于DOM的XML解析数据,随着.NET平台升级到2.0,对象化的配置类型也可以提供基于内存缓冲信息的访问。应用可能要求包装类型提供基于联合索引的查询(尤其对于属性较多、关系复杂的实体),而索引器又成了一个非常优雅的封装方式。
比如:一个员工实体包括“FirstName”、“FamilyName”、“Title”3项属性,我们需要包装一个Staff类型管理全部的员工信息,如图1-12所示。
图1-12 具有联合主键用户实体
同时根据 UI 绑定或其他功能检索的需要,我们会根据它的联合主键(FirstName +FamilyName)提供一个索引器以访问具体的员工记录。示例如下:
1. 完成具有操作联合索引的类
C#
/// 具有联合索引特点的实体类型 public struct Employee { public string FirstName; // PK Field public string FamilyName; // PK Field public string Title; public Employee(DataRow row) { this.FirstName=row["FirstName"] as string; this.FamilyName=row["FamilyName"] as string; this.Title=row["Title"] as string; } } public class Staff { static DataTable data=new DataTable(); /// 数据准备 /// <remarks>实际数据应该会从数据库等持久层渠道获得</remarks> static Staff() { data.Columns.Add("FirstName"); data.Columns.Add("FamilyName"); data.Columns.Add("Title"); // pk : familyname+firstname data.PrimaryKey=new DataColumn[] { data.Columns[0], data.Columns[1] }; data.Rows.Add("Jane", "Doe", "Sales Manger"); data.Rows.Add("John", "Doe", "Vice President"); data.Rows.Add("Rupert", "Muck", "President"); data.Rows.Add("John", "Smith", "Logistics Engineer"); } /// 基于联合PK检索 public Employee this[string firstName, string familyName] { get { DataRow row=data.Rows.Find(new object[] { firstName, familyName }); return new Employee(row); } } }
2. 通过单元测试验证
Unit Test
[TestMethod] public void FindStaff() { Staff staff=new Staff(); Employee employee=staff["John", "Doe"]; string exptected="Vice President"; Assert.AreEqual<string>(exptected, employee.Title); }
通过委托传递索引规则
如上文,对于检索规则固定的情况而言,我们可以通过在索引器内部硬编码完成,但如果要完成一些更为公共的类库,往往还要“授之以渔”,即除了告诉它“要检索”之外,还要告知检索策略和规则。在这方面 C#是非常有优势的,因为它有对象化的托管委托类型(delegate),而且.NET Framework FCL部分也提供了很多现成的委托,所以我们不妨善加利用。
这时候,我们会发现索引器的功能更加强大,就像在使用SQL语句的WHERE 子句一样,根据需要以灵活的方式对目标数据进行筛选。示例代码如下:
C#
public class Dashboard { float[] temps=new float[10] { 56.2F, 56.7F, 56.5F, 56.9F, 58.8F, 61.3F, 65.9F, 62.1F, 59.2F, 57.5F }; ///与SQL语句中Where子句的效果非常类似 /// <param name="predicate">传入的检索规则</param> public float this[Predicate<float> predicate] { get { float[] matches=Array.FindAll<float>(temps, predicate); return matches[0]; } } }
Unit Test
[TestMethod] public void FindData() { float expected=65.9F; Dashboard dashboard=new Dashboard(); float actual=dashboard[ delegate(float data) // Predicate<float>的委托 { return data > 63F; }]; Assert.AreEqual<float>(expected, actual); expected=56.7F; actual=dashboard[ delegate(float data) // 更换规则 { return ((data < 63F) && (data > 56.5F)); }]; Assert.AreEqual<float>(expected, actual); }
不过,在实际使用中WHERE子句可能还会包括不只一条的限制条件,对此,索引器一样可以完成。例如,WHERE语句可以定义为下列代码形式:
C#
///与SQL语句中一组Where子句的效果非常类似 public float this[params Predicate<float>[] predicates] { get { // 具体实现可以参考上面的例子,基本上和我们写SQL的Where类似 // 具体实现略过 throw new NotSupportedException(); } }
LINQ时代的索引器:乍一看,索引器似乎已经越来越接近于LINQ通过Lamada表达式完成的功能,不过有一些区别。
● 定位上索引器一般面向单条检索结果,而不是批量结果(尽管我们可以让索引器返回一个IEnumerable<T>)。
● 从封装和客户程序使用的角度看,LINQ有各种内置并被优化的LINQ to系列,而索引器给客户程序的是一种更贴近业务语义、更加直观的形式,因为客户程序无须编写LINQ查询,按照键值检索即可。
不过,把两者结合使用倒是一个非常不错的组合,索引器做接口,LINQ 完成内部检索逻辑,客户程序在无须记住具体方法名称的前提下,按照键值检索即可,索引器内部则依托LINQ to系列的基础,提供对各种异构数据源的访问。
小结
索引器具有“上善若水”的语言特性,当客户程序访问集合信息的时候,索引器会让代码显得异常的简洁、朴实,示例代码如下:
Conversation
客户程序:“C,你是个集合类型么?” 集合类型:“是” 客户程序:“那好,给我第3项。” 集合类型:“C[2],拿去~~~”
就像我们设计接口时会根据业务领域把各类型的职能分解一样,操作类型同样可以根据访问内容的不同,选择使用不同的访问方法,比如:
● 索引器:承担各种检索和查找的工作。
●属性(Property):承担“它的……特性是……”或“它们的……特质是……”的工作,用来标注某个实例特性(成员属性)或静态特性(静态属性)。
● 普通的方法:承担“让它处理……”的职能。
●而事件定位于“当……发生的时候,要作些……”。
受到惯性影响,我们常常把索引器作为一个仅按照编号反馈结果的入口,但就如SQL 中的WHERE子句,我们可以做的其实很多。善用它就能令我们的程序更加亲切、更加清晰。
1.2.6 融入C#语言的迭代机制——迭代器(Iterator)
除了Template这个最自然面向对象的语言特性外,迭代器恐怕是C#可以提供的最简单的设计模式了。.NET Framework有自己的IEnumerator接口,还有Java觉得不错也“学”过去的foreach。迭代器的作用很明确:提供顺序访问一个内部组成相对复杂类型各个元素的手段,而客户程序无须了解那个复杂的内部组成。
工程环境中,除了Composite、Iterator两个基本的模式可以利用迭代器完成外,出于运行态检查或运行监控的需要,Facade、Flyweight、Interpreter、Mediator、Memento等类的行为性、结构型模式同样可以通过迭代器简化设计,因为它们内部都需要组织一系列对象,而遍历每个成员又是经常性的操作。
基本应用
对同一个事物,不同人会有不同的看法。
对象也一样,虽然都是遍历,但是不同语境下、不同上下文限制下,要遍历的内容可能完全不同。正好,Iterator提供了这样一个机会,对于一个类型,您可以提供多个IEnumerable (泛型或非泛型的),同时还可以提供一个最基本的IEnumerator(泛型或非泛型的)。示例代码如下:
C#
using System; using System.Collections; using System.Collections.Generic; namespace MarvellousWorks.PracticalPattern.Concept.Iterating { public class RawIterator { private int[] data=new int[]{0, 1, 2, 3, 4}; // 最简单的基于数组的全部遍历 // 如果客户程序需要强类型的返回值,可以采用泛型声明public IEnumerator<int> public IEnumerator GetEnumerator() { foreach (int item in data) yield return item; } // 返回某个区间内数据的IEnumerable public IEnumerable GetRange(int start, int end) { for (int i=start; i <= end; i++) yield return data[i]; } // 手工“捏”出来的IEnumerable<string> public IEnumerable<string> Greeting { get { yield return "hello"; yield return "world"; yield return "!"; } } } }
Unit Test
[TestClass()] public class RawIteratorTest { [TestMethod] public void Test() { int count=0; // 测试 IEnumerator RawIterator iterator=new RawIterator(); foreach (int item in iterator) Assert.AreEqual<int>(count++, item); count=1; // 测试具有参数控制的IEnumerable foreach (int item in iterator.GetRange(1, 3)) Assert.AreEqual<int>(count++, item); string[] data=new string[] { "hello", "world", "!" }; count=0; // 测试手工 “捏” 出来的IEnumerable foreach (string item in iterator.Greeting) Assert.AreEqual<string>(data[count++], item); } }
简化复杂结构的遍历
当然,如果仅用迭代器遍历数组有点“杀鸡用牛刀”的感觉,迭代器最适用的场景还是访问复杂结构。这里假想一种情形——早上上班铃响的那一瞬间,想想员工都在什么地方?如图1-13所示。
● 在电梯里,马上就要到办公楼层的,那么我们假设他们都很有礼貌地被保存在一个栈里。
● 在楼下走廊和楼上走廊的,那么也假设他们很有礼貌地被保存在一个队列里。
● 到了办公室里马上就进入了一个有上下级关系的组织,假设他们被保存在二叉树里。
●还有就是那一刻还没有打卡,而列入迟到名单的,姑且算被保存在一个数组里。
图1-13 一个示例的内部对象组织结构
虽然从客户程序端角度来看,其内部结构似乎有些复杂,不过就像我们解决其他复杂问题一样,“分而治之”就好了,关键是为每个部分找到它们的IEnumerator。
● Stack和Stack<T>都可以通过GetEnumerator()获得。
● Queue、Queue<T>、Ayyay也可以通过GetEnumerator()获得。
● 至于那个二叉树,只能自己做了,不过就和遍历二叉树一样,安排好次序即可。
● 在确定了每个部分可以获得IEnumerator之后,需要有一个具有组合关系的容器来保存所有的类型,即便有更多的关系需要遍历也没关系,因为它有组合关系,把它们组合在一起,最后客户程序看到的也就只有一个对象(类似上文的MulticastDelegate)。
1. 设计每个员工——ObjectWithName,其代码如下:
C#
public class ObjectWithName { private string name; public ObjectWithName(string name) { this.name=name; } public override string ToString() { return name; } }
2. 设计那个保存员工信息的二叉树,其代码如下:
C#
public class BinaryTreeNode : ObjectWithName { private string name; public BinaryTreeNode(string name) : base(name) { } public BinaryTreeNode Left=null; public BinaryTreeNode Right=null; public IEnumerator GetEnumerator() { yield return this; if (Left != null) foreach (ObjectWithName item in Left) yield return item; if (Right != null) foreach (ObjectWithName item in Right) yield return item; } }
3. 设计具有组合关系的CompositeIterator,其代码如下:
C#
using System; using System.IO; using System.Collections; using System.Collections.Generic; using System.Reflection; namespace MarvellousWorks.PracticalPattern.Concept.Iterating { public class CompositeIterator { //为每个可以遍历对象提供的容器 // 由于类行为object ,所以CompositeIterator自身也可以嵌套 private IDictionary<object,IEnumerator>items=new Dictionary<object, IEnumerator>(); public void Add(object data) { items.Add(data, GetEnumerator(data)); } //对外提供可以遍历的IEnumerator public IEnumerator GetEnumerator() { if ((items != null) && (items.Count > 0)) foreach (IEnumerator item in items.Values) while (item.MoveNext()) yield return item.Current; } // 获取IEnumerator public static IEnumerator GetEnumerator(object data) { if (data == null) throw new NullReferenceException(); Type type=data.GetType(); // 是否为Stack if (type.IsAssignableFrom(typeof(Stack)) || type.IsAssignableFrom(typeof(Stack<ObjectWithName>))) return DynamicInvokeEnumerator(data); // 是否为Queue if (type.IsAssignableFrom(typeof(Queue)) || type.IsAssignableFrom(typeof(Queue<ObjectWithName>))) return DynamicInvokeEnumerator(data); // 是否为Array if ((type.IsArray) && (type.GetElementType().IsAssignableFrom( typeof(ObjectWithName)))) return ((ObjectWithName[])data).GetEnumerator(); // 是否为二叉树 if (type.IsAssignableFrom(typeof(BinaryTreeNode))) return ((BinaryTreeNode)data).GetEnumerator(); throw new NotSupportedException(); } // 通过反射动态调用相关实例的GetEnumerator方法获取 IEnumerator private static IEnumerator DynamicInvokeEnumerator(object data) { if (data == null) throw new NullReferenceException(); Type type=data.GetType(); return (IEnumerator)type.InvokeMember("GetEnumerator", BindingFlags.InvokeMethod, null, data, null); } } }
4. 单元测试验证,其代码如下:
C#
[TestClass()] public class CompositeIteratorTest { [TestMethod] public void Test() { #region 准备测试数据 // stack<T> Stack<ObjectWithName> stack=new Stack<ObjectWithName>(); stack.Push(new ObjectWithName("2")); stack.Push(new ObjectWithName("1")); // Queue<T> Queue<ObjectWithName> queue=new Queue<ObjectWithName>(); queue.Enqueue(new ObjectWithName("3")); queue.Enqueue(new ObjectWithName("4")); // T[] ObjectWithName[] array=new ObjectWithName[3]; array[0]=new ObjectWithName("5"); array[1]=new ObjectWithName("6"); array[2]=new ObjectWithName("7"); // BinaryTree BinaryTreeNode root=new BinaryTreeNode("8"); root.Left=new BinaryTreeNode("9"); root.Right=new BinaryTreeNode("10"); root.Right.Left=new BinaryTreeNode("11"); root.Right.Left.Left=new BinaryTreeNode("12"); root.Right.Left.Right=new BinaryTreeNode("13"); root.Right.Right=new BinaryTreeNode("14"); root.Right.Right.Right=new BinaryTreeNode("15"); root.Right.Right.Right.Right=new BinaryTreeNode("16"); // 合并所有 IEnumerator对象 CompositeIterator iterator=new CompositeIterator(); iterator.Add(stack); iterator.Add(queue); iterator.Add(array); iterator.Add(root); #endregion int count=0; foreach (ObjectWithName obj in iterator) Assert.AreEqual<string>((++count).ToString(), obj.ToString()); } }
小结
无论您所面对的对象内部结构多么复杂,如果您的任务是封装它们,而不只是把它们全部public了,则尽可能地提供一个Iterator,可以的话按照需要使用的领域各提供一个Iterator(见图1-14),这样您的继任者,当然也可能是“朝花夕拾”的您自己,一定会对当时所做的工作心存感激……
图1-14 通过加入迭代器以缓解和便利复杂对象访问
1.3 可重载运算符(Overloadable Operators)与转换运算符(Conversion Operators)
1.3.1 The Day After Someday
假设有天一上班,领导把你叫过去,说:“用户的计费系统需要修改,除了价格 * 数量外,还要减去折扣,今天中午前最好更新上去”。也就是:
Math
Total=Price * Quantity变成了 Total=Price * Quantity - Discount
你心想:“这有什么难的?马上就可修改好”。
但是发现了一个意想不到的情况:
● 因为不知名的原因,.NET Framework不支持+、-、*、/和=了。
● Automatic Updates已经帮您把公司所有的机器更新过了。
不过有个好消息,之前公司某人封装了一个DoubleNumber类。这么看来没有别的选择了——“我相信第一眼的感觉”,于是您也在很短的时间内更新了程序(方案1),数日后一切正常后,您又用=和算术计算符重写了这个修改(方案2):
C# (方案1)
// 没办法,没有= ,new 出来的DoubleNumber没地方存放 new DoubleNumber().Multiple(price, quantity, out result); new DoubleNumber().Substract(result, discount, out result);
C# (方案2)
return price * quantity - discount;
对方案1有什么感觉呢?恐怕用一个字来形容是“乱”,用两个字来形容则是“麻烦”。不管是喜欢还是发自内心的厌恶,我们都学了很多年数学,已经习惯了等号、不等号还有加减乘除了,对于纯粹计算的类型,您可以调用某些计算方法完成,当然最简单的加减乘除可能还是直接用我们最习惯的数学运算符更直观。比起某些通用的开发语言来说,使用C#的我们还算幸运,可以重载运算符,MSDN中教科书式的复数类(Complex)相信大家都已经很熟悉了,那么同样的方便性是不是也可以用于其他工程化编码中呢?
可以。
1.3.2 用于有限的状态迭代
四季的更迭“不以尧存,不以桀亡”,对于季节类型而言,它本身具有很明显的状态特征。其示例代码如下:
C#
namespace MarvellousWorks.PracticalPattern.Concept.Operator { public class Season { public static readonly string[] Seasons = new string[] { "Spring", "Summer", "Autumn", "Winter" }; private int current; public Season() { current=default(int); } public override string ToString() { return Seasons[current]; } public static Season operator ++(Season season) { season.current=(season.current+1) % 4; return season; } public static implicit operator string(Season season) { return season.ToString(); } } }
Unit Test
[TestClass()] public class SeasonTest { [TestMethod] public void Test() { Season season=new Season(); Assert.AreEqual<string>(Season.Seasons[0], season); season++; season++; Assert.AreEqual<string>(Season.Seasons[2], season); } }
同样地,对于常规意义上的状态图而言,它一样可以区分出前置和后置,到底要转移到哪种状态,需要依靠上下文和规则而定,虽然不像比例中的四季那样严格地周而复始,但一样可以++和--,只不过增加一个因素而已。
1.3.3 操作集合
又碰到集合了。上面说的MulticastDelegate和.NET Framework自身的状态机制都有现成的操作集合的范本:需要Add就 +=,需要Remove就-=。就如在上文提到的委托一样,设计模式中很多地方都可以用重载运算符的办法,让客户程序操作的时候更简洁一些:
● Chain of Responsibility:增加一个链表节点。
● Command:给Invoker增加一个命名对象。
● Memento:增加一个“备忘”的内容项。
● ……
就像Add和Remove可以被重载一样,我们在使用可重载操作符的时候,也要“不拘一格”。其示例代码如下:
C#
using System.Collections.Generic; namespace MarvellousWorks.PracticalPattern.Concept.Operator { public class ErrorEntity { private IList<string> messages=new List<string>(); private IList<int> codes=new List<int>(); public static ErrorEntity operator +(ErrorEntity entity, string message) { entity.messages.Add(message); return entity; } public static ErrorEntity operator +(ErrorEntity entity, int code) { entity.codes.Add(code); return entity; } public IList<string> Messages { get { return messages; } } public IList<int> Codes { get { return codes; } } } }
Unit Test
[TestClass()] public class ErrorEntityTest { [TestMethod] public void Test() { ErrorEntity entity=new ErrorEntity(); entity += "hello"; entity += 1; entity += 2; Assert.AreEqual<int>(1, entity.Messages.Count); Assert.AreEqual<int>(2, entity.Codes.Count); } }
1.3.4类型的适配
设计模式中Adapter承担的是不兼容类型间的适配工作,一般同类书籍中的实现操作会把Adapter设计为继承Target的独立类,但如果有个机制,确保在需要的时候它可以自动地转换为目标接口,是不是也可以达到类似的效果呢?其示例代码如下:
C#
using System; using System.Collections.Generic; namespace MarvellousWorks.PracticalPattern.Concept.Operator { public class Adaptee { // 不兼容的接口方法 public int Code { get { return new Random().Next(); } } } public class Target { private int data; public Target(int data){this.data=data;} // 目标接口方法 public int Data { get{return data;}} // 隐式类型转换进行适配 public static implicit operator Target(Adaptee adaptee) { return new Target(adaptee.Code); } } }
Unit Test
[TestClass()] public class AdapteeTest { [TestMethod] public void Test() { Adaptee adaptee=new Adaptee(); Target target=adaptee; Assert.AreEqual<int>(adaptee.Code, target.Data); List<Target> targets=new List<Target>(); targets.Add(adaptee); targets.Add(adaptee); Assert.AreEqual<int>(adaptee.Code, targets[1].Data); } }
这种做法有什么局限呢?那就是子类“沾不上光”,原因在于转换运算符是static的,子类不能按照继承自动获取这个类型转换的特性。这也就等于提醒我们:如果工程中Target和Adaptee 可以保证 1:1,那么无所谓,直接进行类型转换就可以了;如果觉得隐式转换可能不安全,没关系,定义为显式的即可;如果Target本身就是个接口或抽象类,则最好打消这个想法,尽管抽象类也可以定义转换运算符。
1.3.5 小结
就像本节一开始的“The Day After Someday”一样,没有=、+、*的时候,我们很可能会有“别扭”的感觉;反言之,如果在必要的地方,无论是进行运算符重载还是使用类型转换符,都会为编码提供不少的便利。
1.4 面向插件架构和现场部署的配置系统设计
工程化代码和Example代码另一个最大的区别就是有关配置的处理,Example可以“捏”些参数,甚至定义几个const,但是在工程化代码的编写中,除非您的成果应用领域非常狭小,而且极少需要重新编译并部署,用户也仅仅是有限的几个点,那么无所谓,您全部使用硬编码好了;如果不是,您可能要考虑给代码一个“活口”,通过配置给您的软件更自由、更具适应性的机会。
配置可以有很多种形式:
● App.config或自己写的XML。
● 存在数据库里的参数本身也可以被视为配置。
● .ini文件和.inf文件。
● 令人又爱又恨的注册表。
● 互联网和SOA时代,配置更是无所不在,很有可能费用的计算要基于外管局网页的汇率。
应用使用它们的途径也很多:
● 彻底“放任自流”,应用实体自己封装相关的访问措施。
● 通过通用的配置访问机制,隔离配置的实际物理存储,确保每个应用实体封装的时候仅关心逻辑存储上的各种配置信息。
● 彻底让应用逻辑不知道配置信息的存在,“哪里有配置?满眼都是对象。”这方面Enterprise Library在组合ObjectBuilder+Configuration+Instrument方面作了很好的表率,不过就是稍微重了些。
本书采用一种折中的办法,毕竟设计模式公共库关系的内容本身几乎不涉及具体数据源、日志持久对象之类的内容,相关的配置主要集中在动态加载的类型、相关集合类型的容量控制等,同时为了充分利用.NET Framework 平台的现有机制,相关的配置布局采用完全基于 App.Config的自定义配置解决。为了便于客户程序根据需要选择的模式,每个模式自己独立定义出一个配置节组,名称和相关的Class Library项目的默认命名空间保持一致(不过按照.NET的命名习惯,命名采用首字母小写的方式)。例如,Builder模式:
C# Class Library Default Namespace
namespace MarvellousWorks.PracticalPattern.Builder
相应地配置sectionGroup
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <sectionGroup name="marvellousWorks.practicalPattern.builder" .../> </configSections> <marvellousWorks.practicalPattern.builder/> </configuration>
1.4.1 认识.NET Framework提供的主要配置实体类
本书中,各设计模式的配置全部基于.NET Framework提供的配置实体,并通过.NET自己的app.config解析完成,会涉及的实体基类如下:
● System.Configuration.ConfigurationSectionGroup(配置节组)
仅仅是为了逻辑上组织各个配置节,虽然您可以不使用,或者整个项目就使用一个配置节,但为了确保您的Assembly 可以在其他地方被反复重用,最好将SectionGroup与Assembly进行一对一“搭对”,甚至当Assembly由于历史原因Façade的东西太多的时候,一个Assembly也可以划分为几个SectionGroup。至于划分标准,建议类比SRP原则,以Assembly的每个服务特性为边界,明确划分SectionGroup。
● System.Configuration.ConfigurationSection(配置节)
维护一个独立的配置内容,与ConfigurationElement、ConfigurationElementCollection不同,自定义的ConfigurationSection需要在<configuration><configSections/> </configuration>下注册,同时.NET Framework会以<section/>或其父节点<sectionGroup>作为解析一块配置的入口点。
● System.Configuration.ConfigurationElementCollection(配置元素集合)
配置元素集合本身不需要在<configuration><configSections/></configuration>下注册,它是一组配置元素的父节点。
● System.Configuration.ConfigurationElement(配置元素)
笔者感觉.NET 明确区分这四个类主要是便于开发人员更明确地操作配置文件。实际设计中,笔者建议按照以下步骤设计并解析自己的配置文件:
1. 首先完成配置文件的“骨架”,即先完成配置节组和配置节的操作。
2. 用配置元素集合填充相应的配置节。
3. 设计每个配置元素需要包括的属性。
4. 从最下层开始逐个设计每个配置元素类,这仅需根据需要的属性描述出每一个类即可。
根据以往的经验,好像开发人员在做这部分“照葫芦画瓢”的过程中往往忽略了使用面向对象的继承特性,因此无论是出于后续扩展的需要,还是根据添加如监控之类的企业开发要求,笔者强烈建议:一定要在每个具体配置元素类之下、System.Configuration. ConfigurationElement之上,设计一个自己应用的抽象基类。
本书的主要目的是建立工程化的企业设计模式库,每个模式都要增加独立的配置类,为了便于客户程序的使用和扩展,不仅要有一个统一的配置元素抽象基类,在多数模式中还会继承出一个模式内的专用抽象基类。
5.配置元素集合类本身。这相对要简单些,毕竟它的基本作用就是个容器。出于相同的目的,也要有个“基”,而且最好是个泛型的“基”。
6. 按照最初的“骨架”,实现配置节和配置节组,并把相关的配置元素或配置元素集合组装起来。
7. 在单元测试项目里,生成一个相应的App.Config文件,并用测试信息填充,通过Unit Test确认您的Object - Configuration Mapping设计可以正常运行。
这里有个技巧,因为默认VSTS的Test Project生成的目标App.Config与实际的Assembly不搭对,会在加载配置对象的时候出错,因此需要修改Test Project属性,在Build Events的pre-build event command line增加如下代码及如图1-15所示。
Command Line
copy /Y $(ProjectDir)app.config $(TargetPath).config
图1-15配置项目属性,保证Unit Test项目的App.Config可以被正确地部署到目标目录
1.4.2 应用实例
下面以本章Delegate、Generics两部分的大纲为例,通过System.Configuration相关机制提供一个解析范例。
1. 定义App.Config框架,包括一个ConfigurationSectionGroup和两个ConfigurationSection,每个配置节下面包括一个<examples>和<diagrams>配置元素集合,委托介绍部分还有一个Reflector.Net的截图<picture>,其示例代码如下:
App.Config
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <!-- fake 代表还没有定义对象类型,临时声明一个PlaceHolder --> <sectionGroup name="marvellousWorks.practicalPattern.concept" type="fake"> <section name="delegate" type="fake"/> <section name="generics" type="fake"/> </sectionGroup> </configSections> <!-- 具体配置部分 --> <marvellousWorks.practicalPattern.concept> <delegate> <examples/> <diagrams/> <pictures/> </delegate> <generics> <examples/> <diagrams/> </generics> </marvellousWorks.practicalPattern.concept> </configuration>
2. 定义每个<example>和<diagram>仅包括“name”(Required)和“description”(Optional)两个attribute,而<picture>还需要增加一个是否为彩色的属性“colorized”(Required),相应的配置元素解析类如图1-16所示。
图1-16配置对象结构
示例代码如下:
C#
using System; using System.Configuration; namespace MarvellousWorks.PracticalPattern.Concept.Configurating { // 定义具有name和description属性的配置元素 // name属性作为ConfigurationElementCollection中相应的key public abstract class NamedConfigurationElementBase : ConfigurationElement { private const string NameItem="name"; private const string DescriptionItem="description"; [ConfigurationProperty(NameItem, IsKey=true, IsRequired=true)] public virtual string Name { get { return base[NameItem] as string; } } [ConfigurationProperty(DescriptionItem, IsRequired=false)] public virtual string Description { get { return base[DescriptionItem] as string; } } } public class ExampleConfigurationElement : NamedConfigurationElementBase { } public class DiagramConfigurationElement : NamedConfigurationElementBase { } public class PictureConfigurationElement : NamedConfigurationElementBase { private const string ColorizedItem="colorized"; [ConfigurationProperty(ColorizedItem, IsRequired=true)] public bool Colorized { get { return (bool)base[ColorizedItem]; } } } }
3. 完成保存相应配置元素的容器——配置元素集合。
泛型,还是泛型。另外,声明这些容器为AddRemoveClearMap,如图1-17所示。
图1-17 泛型化的配置对象结构
示例代码如下:
C#
using System; using System.Configuration; namespace MarvellousWorks.PracticalPattern.Concept.Configurating { // 定义包括 NamedConfigurationElementBase的 ConfiugrationElementCollection [ConfigurationCollection(typeof(NamedConfigurationElementBase), CollectionType=ConfigurationElementCollectionType.AddRemoveClearMap)] public abstract class NamedConfigurationElementCollectionBase<T> : ConfigurationElementCollection where T : NamedConfigurationElementBase, new() { // 外部通过index 获取集合中特定的configurationelement public T this[int index] { get { return (T)base.BaseGet(index); } } public new T this[string name] { get { return (T)base.BaseGet(name); } } // 创建一个新的NamedConfiugrationElement实例 protected override ConfigurationElement CreateNewElement() { return new T(); } // 获取集合中某个特定NamedConfiugrationElement的key (Name属性) protected override object GetElementKey(ConfigurationElement element) { return (element as T).Name; } } public class ExampleConfigurationElementCollection : NamedConfiguration- ElementCollectionBase<ExampleConfigurationElement> { } public class DiagramConfigurationElementCollection : NamedConfiguration- ElementCollectionBase<DiagramConfigurationElement> { } public class PictureConfigurationElementCollection : NamedConfiguration- ElementCollectionBase<PictureConfigurationElement> { } }
4. 设计每个配置节的解析类,同时把相应的配置元素集合组装上去,如图1-18所示。
图1-18配置节部分的结构
示例代码如下:
C#
using System; using System.Configuration; namespace MarvellousWorks.PracticalPattern.Concept.Configurating { // 文章段落的配置节类型包括: // 1. <examples>的ConfigurationElementCollection (optional) // 1. <diagrams>的ConfigurationElementCollection (optional) public abstract class ParagraphConfigurationSectionBase : ConfigurationSection { private const string ExamplesItem="examples"; private const string DiagramsItem="diagrams"; [ConfigurationProperty(ExamplesItem, IsRequired=false)] public virtual ExampleConfigurationElementCollection Examples { get { return base[ExamplesItem] as ExampleConfigurationElementCollection; } } [ConfigurationProperty(DiagramsItem, IsRequired=false)] public virtual DiagramConfigurationElementCollection Diagrams { get { return base[DiagramsItem] as DiagramConfigurationElementCollection; } } } public class DelegatingParagramConfigurationSection : ParagraphConfigurationSectionBase { private const string PicturesItem="pictures"; [ConfigurationProperty(PicturesItem, IsRequired=false)] public virtual PictureConfigurationElementCollection Pictures { get { return base[PicturesItem] as PictureConfigurationElementCollection; } } } public class GenericsParagramConfigurationSection : ParagraphConfigurationSectionBase { } }
5. 用配置节组把“冰糖葫芦串一串”,其代码如下:
C#
using System; using System.Configuration; namespace MarvellousWorks.PracticalPattern.Concept.Configurating { // 整个配置节组的对象, 包括<delegating>和<generics>两个配置节 // <sectionGroup name="marvellousWorks.practicalPattern.concept"/> public class ChapterConfigurationSectionGroup : ConfigurationSectionGroup { private const string DelegatingItem="delegating"; private const string GenericsItem="generics"; public ChapterConfigurationSectionGroup() : base() { } [ConfigurationProperty(DelegatingItem, IsRequired=true)] public virtual DelegatingParagramConfigurationSection Delegating { get{return base.Sections[DelegatingItem] as DelegatingParagramConfigurationSection;} } [ConfigurationProperty(GenericsItem, IsRequired=true)] public virtual GenericsParagramConfigurationSection Generics { get { return base.Sections[GenericsItem] as GenericsParagramConfigurationSection; } } } }
6. 最后通过一个公共的ConfigurationBroker类统一对外每个配置节的访问,在Test Project的App.Config中填充测试配置信息,并通过Unit Test进行验证确认,其代码如下:
C#
// 用于调度App.Config 相关Configuration的Broker类型 public static class ConfigurationBroker { private static ChapterConfigurationSectionGroup group; static ConfigurationBroker() { Configuration config=ConfigurationManager.OpenExeConfiguration( ConfigurationUserLevel.None); group=(ChapterConfigurationSectionGroup)config.GetSectionGroup ("marvellousWorks.practicalPattern.concept"); } public static DelegatingParagramConfigurationSection Delegating { get { return group.Delegating; } } public static GenericsParagramConfigurationSection Generics { get { return group.Generics; } } }
App.Config
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <!-- fake 代表还没有定义对象类型,临时声明一个PlaceHolder --> <sectionGroup name="marvellousWorks.practicalPattern.concept" type= "MarvellousWorks.PracticalPattern.Concept.Configurating. ChapterConfigurationSectionGroup,Concept"> <section name="delegating" type= "MarvellousWorks.PracticalPattern.Concept.Configurating. DelegatingParagramConfigurationSection,Concept"/> <section name="generics" type= "MarvellousWorks.PracticalPattern.Concept.Configurating. GenericsParagramConfigurationSection,Concept"/> </sectionGroup> </configSections> <!-- 具体配置部分 --> <marvellousWorks.practicalPattern.concept> <delegating> <examples> <add name="AsyncInvoke" description="用Delegate完成异步调用"/> <add name="MulticastNotify" description="1对n的通知"/> <add name="AnonymousMethod" description="调用匿名方法"/> </examples> <diagrams> <add name="DelegateList" description="Delegate集合与多个抽象方法关联"/> <add name="CompositeDelegate" description="MulticastDelegate实现机制"/> </diagrams> <pictures> <add name="EventHandler" colorized="true"/> </pictures> </delegating> <generics> <examples> <add name="RawGenericFactory" description="通用工厂类"/> <add name="GenericAdapter"/> </examples> <diagrams> <add name="ClassicBuilder" description="经典Builder模式实现"/> </diagrams> </generics> </marvellousWorks.practicalPattern.concept> </configuration>
Unit Test
[TestClass()] public class ConfigurationBrokerTest { [TestMethod] public void Test() { DelegatingParagramConfigurationSection s1 = ConfigurationBroker.Delegating; Assert.IsTrue(s1.Pictures["EventHandler"].Colorized); Assert.AreEqual<string>( "1对n的通知", s1.Examples["MulticastNotify"].Description); GenericsParagramConfigurationSection s2 = ConfigurationBroker.Generics; Assert.AreEqual<int>(1, s2.Diagrams.Count); } }
1.4.3 小结
按照.NET Framework“4件套”的八股要求编写代码,相信您可以更轻松地为应用、为自己的Assembly 建立起完整的配置支持。只要在编码的时候,如果突然想到“这个数、这个连接串、这个Uri 是不是放到生产环境才能大概知道”,不要多想了,增加个配置节或配置元素,交给运维的人去管理吧。
编码的时候多八股几行代码,系统上线以后,相对会省心一些,尤其当你的代码被无数次复用,或者作为一个销路不错的产品的关键组成时,也许体会会更好一些。
不过,之前提到的单一职责原则同样适用,每一个配置项服务的内容尽量单一些,否则,一个配置项负责的职责增多以后,相应的变化因素也随之增加。
1.5实现依赖注入
1.5.1 背景介绍
设计模式中,尤其是结构型模式很多时候解决的就是对象间的依赖关系,变依赖具体为依赖抽象。平时开发中如果发现客户程序依赖某个(或某类)对象,我们常常会对它们进行一次抽象,形成抽象的抽象类、接口,这样客户程序就可以摆脱所依赖的具体类型。
这个过程中有个环节被忽略了——谁来选择客户程序需要的满足抽象类型的具体类型呢?通过后面的介绍你会发现很多时候创建型模式可以比较优雅地解决这个问题。但另一问题出现了,如果您设计的不是具体业务逻辑,而是公共库或框架程序,这时候您是一个“服务方”,不是您调用那些构造类型,而是它们把抽象类型传给您,怎么松散地把加工好的抽象类型传递给客户程序就是另一回事了。
这个情形也就是常说的“控制反转”,IOC:Inverse of Control;框架程序与抽象类型的调用关系就像常说的好莱坞规则:Don't call me, I'll call you.
参考 Martin Fowler 在《Inversion of Control Containers and the Dependency Injection pattern》一文,我们可以采用“依赖注入”的方式将加工好的抽象类型实例“注入”到客户程序中,本书的示例也将大量采用这种方式将各种依赖项“注入”到模式实现的外部——客户程序。下面我们结合一个具体的示例看看为什么需要依赖注入,以及Martin Fowler文中提到的三种经典方式,然后依据 C#语言的特质,再扩展出一个基于 Attribter方式注入(参考原有的Setter命名,这里将基于Attribute的方法称为Attributer)。
1.5.2 示例情景
客户程序需要一个提供System.DateTime类型当前系统时间的对象,然后根据需要仅仅把其中的年份部分提取出来,因此最初的实现代码如下:
C#
using System; using System.Diagnostics; namespace MarvellousWorks.PracticalPattern.Concept.DependencyInjection.Example1 { class TimeProvider { public DateTime CurrentDate { get { return DateTime.Now; } } } public class Client { public int GetYear() { TimeProvider timeProvier=new TimeProvider(); return timeProvier.CurrentDate.Year; } } }
后来因为某种原因,发现使用.NET Framework自带的日期类型精度不够,需要提供其他来源的TimeProvider,确保在不同精度要求的功能模块中使用不同的TimeProvider。这样问题集中在TimeProvider的变化会影响客户程序,但其实客户程序仅需要抽象地使用其获取当前时间的方法。为此,增加一个抽象接口,确保客户程序仅依赖这个接口ITimeProvider,由于这部分客户程序仅需要精确到年,因此它可以使用一个名为SystemTimeProvider (:ITimeProvider)的类型。新的实现代码如下:
C#
using System; namespace MarvellousWorks.PracticalPattern.Concept.DependencyInjection.Example2 { interface ITimeProvider { DateTime CurrentDate { get;} } class TimeProvider : ITimeProvider { public DateTime CurrentDate { get { return DateTime.Now; } } } public class Client { public int GetYear() { ITimeProvider timeProvier=new TimeProvider(); return timeProvier.CurrentDate.Year; } } }
这样看上去客户程序后续处理权都依赖于抽象的ITimeProvider,问题似乎解决了?没有,它还要知道具体的SystemTimeProvider。因此,需要增加一个对象,由它选择某种方式把ITimeProvider实例传递给客户程序,这个对象被称为Assembler。新的结构如图1-19所示。
图1-19 增建装配对象后新的依赖关系
其中,Assembler的职责如下:
● 知道每个具体TimeProviderImpl的类型。
● 可根据客户程序的需要,将抽象ITimeProvider反馈给客户程序。
● 本身还负责对TimeProviderImpl的创建。
下面是一个Assembler的示例实现:
C#
public class Assembler { /// <summary> /// 保存“抽象类型/实体类型”对应关系的字典 /// </summary> private static Dictionary<Type, Type> dictionary = new Dictionary<Type, Type>(); static Assembler() { // 注册抽象类型需要使用的实体类型 //实际的配置信息可以从外层机制获得,例如通过配置定义 dictionary.Add(typeof(ITimeProvider), typeof(SystemTimeProvider)); } /// 根据客户程序需要的抽象类型选择相应的实体类型,并返回类型实例 /// <returns>实体类型实例</returns> public object Create(Type type) // 主要用于非泛型方式调用 { if ((type == null) || !dictionary.ContainsKey(type)) throw new NullReferenceException(); Type targetType=dictionary[type]; return Activator.CreateInstance(targetType); } /// <typeparam name="T">抽象类型(抽象类/接口/或者某种基类)</typeparam> public T Create<T>() // 主要用于泛型方式调用 { return (T)Create(typeof(T)); } }
1.5.3 Constructor注入
构造函数注入,顾名思义,就是在构造函数的时候,通过Assembler或其他机制把抽象类型作为参数传递给客户类型。这种方式虽然相对其他方式有些粗糙,而且仅在构造过程中通过“一锤子”方式设置好,但很多时候我们设计上正好就需要这种Read Only的注入方式。其实现方式如下:
C#
/// 在构造函数中注入 class Client { private ITimeProvider timeProvider; public Client(ITimeProvider timeProvider) { this.timeProvider=timeProvider; } }
Unit Test
[TestClass] public class TestClient { [TestMethod] public void Test() { ITimeProvider timeProvider = (new Assembler()).Create<ITimeProvider>(); Assert.IsNotNull(timeProvider); // 确认可以正常获得抽象类型实例 Client client=new Client(timeProvider); // 在构造函数中注入 } }
1.5.4 Setter注入
Setter注入是通过属性赋值的办法解决的,由于Java等很多语言中没有真正的属性,所以Setter注入一般通过一个set()方法实现,C#语言由于本身就有可写属性,所以实现起来更简洁,更像Setter。相比较Constructor方式而言,Setter给了客户类型后续修改的机会,它比较适应于客户类型实例存活时间较长,但Assembler修改抽象类型指定的具体实体类型相对较快的情景;不过也可以由客户程序根据需要动态设置所需的类型。其实现方式如下:
C#
/// 通过Setter实现注入 class Client { private ITimeProvider timeProvider; public ITimeProvider TimeProvider { get { return this.timeProvider; } // getter本身和以Setter方式实现 注入没有关系 set { this.timeProvider=value; } // Setter } }
Unit Test
[TestClass] public class TestClient { [TestMethod] public void Test() { ITimeProvider timeProvider = (new Assembler()).Create<ITimeProvider>(); Assert.IsNotNull(timeProvider); // 确认可以正常获得抽象类型实例 Client client=new Client(); client.TimeProvider=timeProvider; // 通过Setter实现注入 } }
1.5.5接口注入
接口注入是将包括抽象类型注入的入口以方法的形式定义在一个接口里,如果客户类型需要实现这个注入过程,则实现这个接口,客户类型自己考虑如何把抽象类型“引入”内部。实际上接口注入有很强的侵入性,除了要求客户类型增加需要的Setter或Constructor注入的代码外,还显式地定义了一个新的接口并要求客户类型实现它。除非还有更外层容器使用的要求,或者有完善的配置系统,可以通过反射动态实现接口方式注入,否则笔者并不建议采用接口注入方式。
既然Martin Fowler文中提到了这个实现方式,就给出如下示例:
C#
/// 定义需要注入ITimeProvider的类型 interface IObjectWithTimeProvider { ITimeProvider TimeProvider { get;set;} } /// 通过接口方式注入 class Client : IObjectWithTimeProvider { private ITimeProvider timeProvider; /// IObjectWithTimeProvider Members public ITimeProvider TimeProvider { get { return this.timeProvider; } set { this.timeProvider=value; } } }
Unit Test
[TestClass] public class TestClient { [TestMethod] public void Test() { ITimeProvider timeProvider=(new Assembler()).Create<ITimeProvider>(); Assert.IsNotNull(timeProvider); // 确认可以正常获得抽象类型实例 IObjectWithTimeProvider objectWithTimeProvider=new Client(); objectWithTimeProvider.TimeProvider=timeProvider; // 通过接口方式注入 } }
1.5.6 基于Attribute实现注入——Attributer
如果做个归纳,Martin Fowler之前所介绍的三种模式都是在对象部分进行扩展的,随着语言的发展(.NET从1.0开始,Java从5开始),很多在类元数据层次扩展的机制相继出现,比如C#可以通过Attribute将附加的内容注入到对象上。直观上的客户对象有可能在使用上做出让步以适应这种变化,但这违背了依赖注入的初衷,三个角色(客户对象、Assembler、抽象类型)之中两个不能变,那只好在Assembler上下功夫,谁叫它的职责就是负责组装呢?
为了实现上的简洁,上面三个经典实现方式实际将抽象对象注入到客户类型都是在客户程序中(也就是那三个Unit Test部分)完成的,其实同样可以把这个工作交给Assembler完成;而对于 Attribute方式注入,最简单的方式则是直接把实现了抽象类型的Attribute 定义在客户类型上,例如:
C#
(错误的实现情况) class SystemTimeAttribute : Attribute, ITimeProvider { … } [SystemTime] class Client { … }
相信您也发现了,这样虽然把客户类型需要的ITimeProvider通过“贴标签”的方式告诉它了,但事实上又把客户程序与SystemTimeAttribute“绑”上了,它们紧密地耦合在一起。参考上面的三个实现,当抽象类型与客户对象耦合的时候我们引入了Assembler,当Attribute方式出现类似的情况时,我们写个AttributeAssembler不就行了么?还不行。设计上要把Attribute 设计成一个“通道”,考虑到扩展和通用性,它本身要协助AttributeAssembler 完成ITimeProvider的装配,最好还可以同时装载其他抽象类型来修饰客户类型。示例代码如下:
C#
[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(this.type); } /// 客户类型需要的抽象对象类型 public Type Type { get { return this.type; } } } /// 帮助客户类型和客户程序获取其Attribute定义中需要的抽象类型实例的工具类 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))] // 应用Attribute,定义需要将ITimeProvider通过它注入 class Client { public int GetYear() { //与其他方式注入不同的是,这里使用的ITimeProvider来自自己的Attribute ITimeProvider provider = AttributeHelper.Injector<ITimeProvider>(this); return provider.CurrentDate.Year; } }
Unit Test
[TestMethod] public void Test() { Client client=new Client(); Assert.IsTrue(client.GetYear() > 0); }
1.5.7 小结
依赖注入虽然被Martin Fowler称为一个模式,但平时使用中,它更多地作为一项实现技巧出现,开发中很多时候需要借助这项技巧把各个设计模式所加工的成果传递给客户程序。各种实现方式虽然最终目标一致,但在使用特性上有很多区别。
● Constructor方式:它的注入是一次性的,当客户类型构造的时候就确定了。它很适合那种生命期不长的对象,比如在其存续期间不需要重新适配的对象。此外,相对Setter方式而言,在实现上Constructor可以节省很多代码;
● Setter方式:一个很灵活的实现方式,对于生命期较长的客户对象而言,可以在运行过程中随时适配;
●接口方式:作为注入方式具有侵入性,很大程度上它适于需要同时约束一批客户类型的情况;
●属性方式:随着开发语言的发展引入的新方式,它本身具有范围较小的固定内容侵入性(一个DecoratorAttribute),它也很适合需要同时约束一批客户类型情景。它本身实现相对复杂一些,但客户类型使用的时候非常方便——“打标签”即可。