JGTM'2008 [MVP]

Thinking in WPF...
随笔 - 36, 评论 - 453, 引用 - 16

导航

标签

每月存档

最新留言

广告

偶的AOP文章之Part II及微软无线桌面套装精英版

在很多朋友的一再催促下,偶关于.NET中实现基本AOP编程的第二篇文章终于在上个周末出炉了。这里首先多谢在大周末帮我做review的朋友们,多谢你们的火眼金睛,群众的眼睛真的是够靓啊!!:)

 

在写这篇文章的时候,我从MS的e-CompanyStore新收到的微软无线桌面套装精英版给我添了不少乐趣,可也带来了不少烦恼。键盘的手感虽比不上我那台老的IBM金属键盘,却仍是舒适、便捷得很(尤其是那条皮质的腕托,还有左面的纵横滚轮——可惜不能按!);鼠标虽然不是想象中的皮革版(明明在买它的时候看到图上照片是皮的嘛!上当!),不过仍是十分的漂亮且舒服。不过要命的是,无线的键盘经常会“批发”多余的按键,让人防不胜防(我按一下DELETE键没准给我发出6个!十分钟至少一次的说——怀疑是不是买到了次品?别的用微软无线键盘的朋友有遇到这个问题吗?);另外F/Lock锁定键的默认状态是快捷功能而不是功能键,且没有直观显示,难道F键要下岗了吗?这一点实在有些自作聪明了(其实这些双态键不妨设计为机械式的锁定按键,用键位的高低来显示状态,既不费电,也很直观,而且可以保持用户的习惯——顶多就是内部走线复杂一些,可也对得起这价钱啊)。还有,F功能键从4键x3组变成了3键x4组,更是让我头疼不已(所以没有把它拿回家用——回家我就是开开Flight Simulator 2004,它的键位可是按照4个一组来设计的,所以基本上是没法用了——不过Visual Studio受影响也不小啊)!看来这款“新颖的”产品应该更适合于那些从来没有用过传统键盘布局的朋友吧——不然你且得适应一下呢(而且适应以后就更用不惯不是这样设计的键盘了,还真是微软的一贯风格嘿)。

 

嗯,总之呢,写文章还是挺辛苦的,不过只要能有几个朋友读过文章后有所收获的话,我也就心满意足了。当然,如果你觉得有什么不吐不快的意见和建议啥的,也可以通过博客堂随时与我交流(写评论或者用联系链接),多谢啦!:)

posted on 2004-02-23 19:45:00 by jgtm2000  评论(8) 阅读(5436)

A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)

继续文章的第一部分,我们在这一篇文字中将从另一个角度考虑原文中提出的问题,并深入探索.NET/CLR中提供的相关技术机制,最终以一种AOP的形式达成同样的结果。为了让你能够尽快进入状态,我们先简要回顾一下前文中已经探讨的内容:

 

在文章的第一部分,我们从一个非常简单的虚构的业务操作类(Calculator)开始,结合企业开发中经常会面临的种种非功能性需求(操作日志、权限控制、性能监测等等),用面向对象的思路,结合DECORATOR设计模式,将业务操作对象的核心功能和对它的其他服务性功能代码分离,实现了更高内聚、松散耦合的软件组件。

 

不过在解决了对象代码职责混淆的问题(也就是提高了代码的内聚性)以后,我们却引入了很多的代码,包括为了解决问题而引入的更多的对象及更复杂的结构(用jjx的话说就是“过度设计”——虽然我强调这是一种能够解决我们必须要解决的问题的相对更合理的设计,但我也正是希望读者能在这样的感觉中体验本篇中讨论的方法相对的优越性——看来jjx果然中了偶的这招欲擒故纵:)

 

新引入的问题在相似的业务操作类的数量持续增长的时候变得愈加突出。在我们当前的企业级应用项目中,业务组件的数量已经是数十个了。而我们对每个组件都有几个基本的非业务性需求:基于方法调用颗粒度的权限控制、方法调用日志、业务操作审核、翔实的性能监测、可配置的缓冲策略……这些需求在一次又一次的企业级项目中开发了一次又一次。你说了不是有Application Building Block吗?别忘了,那只是你能用的工具而已,就算每个功能的实现只需要一条语句,想想看,50个组件乘上20个方法……怎么样?受够了吗?结果技术总监说了一句“能不能把我们现在用得自己写的缓冲控制器换成MS新发布的缓冲控制应用构建块?”……

 

可是这么多面向对象的设计方法还有设计模式难道还不能解决这些问题吗?为什么给这个组件的每个方法施加的缓冲控制逻辑不能在另一个组件的另一个方法上重用呢?为什么在一个组件的两个方法上写的逻辑几乎一模一样的方法日志逻辑不能合并呢?我想答案可能就是面向对象中的封装机制——方法,这已经是对象封装在内部的一个实现细节了,在这个层次上你已经回到了结构化编程的世界——你可以调用另一个方法,传入你想传递的参数,但是这个调用就再也不能够省却或合并。既然OOP这种生产力已经不适应新的生产关系,势必产生对新的生产技术的需求——这个新的产物就是所谓的AOP。抽象点儿说,AOP是一种在对象内部层次元素上(主要是构造器和方法)横切的功能植入机制;简单说,AOP允许你拦截方法并植入新的代码(不过现在技术的演变已经朝着越来越复杂的方向发展了),而最关键的是,这种横切是跨越对象类型、甚至与对象类型无关的。我们在本文中就来研究如何利用.NET/CLR中提供的技术机制来用一个类就实现为所有的50个组件的1000个方法拦截并植入我们的非业务性需求代码。

 

好,废话少说,我们切入正题。还是从最简单的例子说起(还是那句话:希望你能够将其想象为更复杂、更真实的情形——不然对于这样简单的事情而言任何设计技术都难逃过度设计之嫌了)

 

public class Calculator
{
  public int Add(int x, int y)
  {
    return x+y;
  }
}

 

这里是基于NUnit的单元测试代码:

 

[TestFixture]
public class UnitTest
{
  public void TestCalculator()
  {
    Calculator calc = new Calculator();
    Assert.IsNotNull(calc);

    Assert.AreEqual(8, calc.Add(3,5));
  }
}

 

还是同前一部分一样的需求,我们先来为这个类添加方法调用日志。这一次我们用一个新的设计模式PROXY来进行思考。其实,PROXY的结构和DECORATOR基本上是一样的,这两个模式的主要区别在于其意图DECORATOR主要用于为对象添加职责;而PROXY则主要用于控制/掌握对对象的访问。现在,我们希望有一个PROXY在调用代码和真实对象之间负责掌握/控制对对象的访问,同时还要客户代码无需了解其存在。为了应用该模式,我们还是逃不开抽象基类或接口、引入工厂等步骤,那么我们首先用工厂方法把对象的创建过程封装起来:

 

public class Calculator
{
  private Calculator() {}

  public static Calculator CreateInstance()
  {
    return new Calculator();
  }


  public int Add(int x, int y)
  {
    return x+y;
  }
}

 

因为默认的无参数构造器已经被修饰为内部可见性private了,所以原来使用new语句的测试代码就无法编译通过了,我们将测试代码相应调整到使用新提供的静态工厂方法调用上:

 

public class UnitTest
{
  public void TestCalculator()
  {
    Calculator calc = Calculator.CreateInstance();
   
  }
}

 

现在我们看看如何可以将一个代理嵌入到调用代码和真实对象之间,显然我们应该在对象创建的过程中动动手脚,比如这样:

 

public class Calculator
{
 
  public static Calculator CreateInstance()
  {
    return (Calculator)new LoggingProxy(new Calculator());
  }
}

 

在上面假想的代码中,我们希望把一个真实对象的新建实例(new Calculator())作为构造参数传入代理对象的构造器,因为最终真正干活的还是我们的真实对象,肯定要把这个家伙传给代理对象。然后我们希望创建好的代理对象应该能够以真实对象的身份(即Calculator类)返回给调用代码。然而,以我们已有的对C#面向对象编程的知识而言,只有当LoggerProxyCalculator的派生类的时候,上面的类型转换代码才可能在运行期成立。而Calculator本身已经是具体类了,让LoggerProxy从中派生恐怕没有道理,所有为了能够有一个能够与之平行兼容的代理类,我们只能为他们提取公共基类或抽象接口(如ICalculator),然后分别派生,再想办法用工厂组合起来……如此一来就等于回到了用DECORATOR模式解决问题的老路上,不是吗?:)

 

不过,如果能有办法让LoggerProxy类具备“模仿”其他类的能力,或者说——使其对于调用代码而言看上去和被代理的类毫无二致的话,前面的代码就能够成立啦!所以我们需要一个所谓的透明代理transparent proxy,也简称TP)!好消息:CLR里面还真有这么个透明代理的类(__TransparantProxy);不幸的是:我们既不能让自己的代理类从透明代理类派生以获得这种能力(正如大多数人希望的那样),也不能通过自定义属性、实现标志性接口等等方法让CLR认为我们的一个类能够透明的“模仿”另一个类。要想在CLR中获取一个透明代理,我们实际上需要提供一个真实代理(real proxy,下简称RP)。

 

一个真实代理是一个从System.Runtime.Remoting.Proxies.RealProxy派生而来的类。这个RealProxy类的首要功能就是帮我们在运行期动态生成一个可以透明兼容于某一个指定类的透明代理类实例。怎么告诉它我们想要“模仿”的类呢?你需要在从RealProxy类派生出来的真实代理类的构造器中显式调用该类中的一个protected RealProxy(Type classToProxy)构造器,传入我们需要透明代理去模仿的那个类型,如下代码所示:

 

using System.Runtime.Remoting.Proxies;

public class MyRealProxy: RealProxy
{
  public MyRealProxy(Type classToProxy): base(classToProxy)
  {
   
  }
}

 

这样,当构造MyRealProxy类的新实例时,RealProxy就会帮我们在内部构造好一个能够透明的模拟classToProxy类的透明代理!而当你得到这个新的真实代理的实例后,你就可以使用其GetTransparentProxy()方法取得其内部已经构造好的透明代理了。为了验证透明代理模仿可以模仿任何类型的超凡能力,请在单元测试中添加并运行这段测试代码:

 

public void TestTransparentProxy()
{
  Type classToProxy = typeof(Calculator);
  RealProxy realProxy = new MyRealProxy(classToProxy);
  object transparentProxy = realProxy.GetTransparentProxy();

  Assert.IsNotNull(transparentProxy);
  Assert.IsTrue(classToProxy.IsInstanceOfType(transparentProxy));
}

 

我们首先选择一个要代理的类型(classToProxy),然后为它构造我们真实代理(realProxy),再从创建好的真实代理实例中取出内部已经动态生成的能够模仿要代理类型的透明代理实例(transparentProxy)。接下来我们验证两件事:首先我们的透明代理不是空引用(说明确实成功的构造出了一个透明代理);然后用Type.IsInstanceOfType()方法验证该对象的类型确实是之前希望模仿的类型(当然你也可以写成检测静态类型的形式,即Assert.IsTrue(transparentProxy is Calculator),不过用代码中的这个方法是可以动态测试类型的)……

 

(靠……蒙我!编译不过去!)嘿嘿,想学习又懒得动手的朋友还是活动活动,把上面的代码实际验证一下,这样印象才深噢!:)

 

其实有问题才好,新的问题恰恰是引领我们学习新事物的动力嘛@#$#%$&*&%……还是让我们先来解决编译不过的问题吧。看看出错信息就知道:我们自己定义的真实代理类(MyRealProxy)没有实现一个叫做Invoke()的方法。翻翻文档,发现这个方法接一个类型为System.Runtime.Remoting.Messaging.IMessage的参数,并返回一个同样类型的对象——这是什么东东?先不管啦,实现了再说(稍后我马上会仔细解释这个的)!谁让RealProxy是一个抽象的基类呢,无论如何你也要记得在从该类派生时实现这个方法才行:

 

using System.Runtime.Remoting.Messaging;

public class MyRealProxy: RealProxy
{
 
  public override IMessage Invoke(IMessage msg)
  {
   
return null;
  }

}

 

使用Visual C#.NET 2003版本的朋友有福了,如果你背不下来也懒得自己去查要override的方法的定义的话,只要在编辑器中先打入“override”,然后在你按下空格之后——“噻,早不告诉我……”

 

这次编译肯定没有问题了,运行NUnit执行TestTransparentProxy测试节点,你马上将发现第二个问题了(够狠——一次下俩套儿):在RealProxy(Type classToProxy)构造器执行时产生了一个异常,说classToProxy必须是一个从MarshalByRefObject类型派生而来的类型。这又是个什么东东?我们还是暂且放下留待以后再说。其实熟悉或者用过.NET Remoting的朋友都了解,要想让一个对象能走出AppDomain周游天下的话,它或者要是MBVmarshal by value),或者要是MBRmarshal by reference)——而成为MBR的方法就是让类型从这个古怪的MarshalByRefObject派生出来(至于成为MBV对象的方法,有两种,请顺便复习一下啦!)

 

因此我们遵循CLR的抗议,将我们的Calculator类的基类指定为MarshalByRefObject(或其派生类):

 

public class Calculator: MarshalByRefObject
{
 
}

 

这次再运行测试,你将见到绿色的通过标志,这就验证了我们对于TP/RP的基本认识(注:如果你没有使用NUnit也无妨,将项目创建或修改成Console类型,然后用Console.WriteLine()输出我们在Assert后面需要验证的结果就行了——比如Console.WriteLine(transparentProxy!=null)然后看它是不是True也是可以的——不过你真的还不想装个NUnit吗?:)

 

现在我们回过头来看看刚才说过的IMessage是什么东西。查一下该接口的定义,发现里面就有一个Properties属性,类型是IDictionary,这说明IMessage只是一个用字典提供数据的消息。想知道这到底是个什么消息,我们就要研究一下这个字典里面都有什么数据。那就让我们来看看吧——怎么看呢?我们注意到这个IMessage是在我们真实代理类的Invoke()方法被调用的时候传入的,显然我们应该在这个方法里面来检查传入的消息。可是谁将会调用这个方法呢?它又是在什么时候调用这个方法呢?让我们首先来回忆一下对方法的调用是如何进行的吧(画面逐渐淡去颜色)……

 

在冯氏计算机体系中,调用方法都是透过堆栈进行的。堆栈是调用代码和被调用代码之间传递参数数据和执行结果的一个数据区。即使是在今天面向对象编程的世界中(乃至延展到今天的.NET世界中),普通的方法调用仍然是经由堆栈进行的。然而我们的高级代码对这一底层机制是毫不知情的,我们只是在进入方法后获得了方法传入的所有参数,并在返回的时候把返回值return给调用者(当然还有所有的ref/out参数的值)就万事大吉。换言之,因为我们的高级代码无法直接操纵堆栈,我们只能在方法的层次上解释参数并返回结果,这样就很难为现有方法嵌入额外代码。还记得我们是怎样利用DECORATOR模式解决这一问题的吗?当调用代码将输入参数传递给某个DECORATOR的某个方法时,我们可以在该方法内部检查甚至修改这些参数,然后再次利用方法调用的机制将调用转发给后一个DECORATOR的某个方法,直到方法调用抵达内部的核心对象再原路返回。这个过程实际上是一系列的构造/解析方法调用堆栈的过程。而利用.NET/CLR中的透明代理机制,情况发生了根本性的改变(逐渐恢复到彩色画面)……

 

当调用代码取得了一个透明代理实例并将其视为一个真实对象发出任何方法调用时,这个透明代理将利用内部机制拦截到该方法调用以及堆栈上所有与之相关的数据(传入的参数值、参数地址等),并将这些数据存储到一个高级代码能够处理的数据结构中,并将这个数据结构转发到能够处理它的代码上。正如你所想象的,这里所谓的“高级代码能够处理的数据结构”就是前面我们看到的IMessage(更具体的说——其中提供的数据字典);而那个“能够处理它的代码”自然就是我们真实代理对象内部的代码咯!也就是说,透明代理帮我们截获了来自调用代码基于堆栈的所有方法调用,并将其打包成数据字典以方法调用消息的形式转发给我们的真实代理进行在高级语言层次上的处理——这就是本篇文字要讲述的核心问题,即利用CLRTP/RP机制拦截方法调用,实现基本的AOP编程任务——通过这里初步的介绍,想必你已经对这种机制与基于传统面向对象(包括前文所述的DECORATOR设计模式)所采用的机制的区别有了初步的感觉。

 

初步了解了这些理论知识,我们不妨来看看透明代理都给我们打包了关于调用方法的什么数据。首先,我们改改Calculator类的CreateInstance()工厂方法,使其返回一个能够模仿Calculator类的透明代理,而这个透明代理所依赖的真实代理不妨就是刚才我们写的那个什么活都不干(其实是还干不了)的MyRealProxy吧!

 

public class Calculator: MarshalByRefObject
{
  public static Calculator CreateInstance()
  {
    RealProxy realProxy = new MyRealProxy(typeof(Calculator));
    object transparentProxy = realProxy.GetTransparentProxy();
    return (Calculator)transparentProxy;

  }
}

 

现在这段代码对你而言应该已经很容易理解了吧(不然还是我没写清楚喽)!编译后,运行最开始的TestCalculator测试,唔……出错喽!看看出错时的调用堆栈就发现其实原因很简单,我们在真实代理的Invoke()方法中什么也没干直接扔回去一个null——这要能干活才怪!不过出错也不要紧,我们还是可以先来检查一下到底传进来的IMessage的属性字典里面都有啥子名堂:

 

public class MyRealProxy: RealProxy
{
 
  public override IMessage Invoke(IMessage msg)
  {
    IDictionary properties = msg.Properties;
    foreach (object key in properties.Keys)
    {
      Console.WriteLine("{0} = {1}", key, properties[key]);
    }


    return null;
  }
}

 

我们知道,一个IDictionary数据字典其实是一个key/value值对数组。在这段新加入的代码中我们枚举字典中的每一个键值,并打印出它和它在字典中的值(你说了我傻了吧,字典中的每一个条目都是一个DictionaryEntry呀,应该foreach (DictionaryEntry entry in msg.Properties)entry.Keyentry.Value访问才正点啊……可惜,这个字典并不是Hashtable那个分支上面的,所以比较古怪,有兴趣你可以去看看它的源码:)。再次运行测试TestCalculator节点,仍然出错!也是,我们是还没解决问题呢。不过即使出错我们也已经在Console.Out窗口中偷窥到下面这样的输出结果了:

 

__Uri=
__MethodName=Add
__MethodSignature=System.Type[]
__TypeName=AOP.Part.II.Calculator, AOP.Part.II, Version=1.0…

__
Args=System.Object[]
__CallContext=System.Runtime.Remoting.Messaging.LogicalCallContext

 

果然,这个字典中包含有关于本次方法调用的一些信息,猜也差得差不多了:__MethodName显然就是被调用的方法的名字,__TypeName则是这个方法所在的类型的全称,__Args是一个object[],它应该是方法调用时候传进来的参数值吧?那__Uri又是什么东东呢?__MethodSignature这个Type[]又是什么呢?还有一个__CallContext,看上去有点儿像前面在DECORATOR中引入的Context,是不是呢?再写两行代码分析一下:

 

public class MyRealProxy: RealProxy
{
 
  public override IMessage Invoke(IMessage msg)
  {
   
    foreach (object arg in (object[])msg.Properties["__Args"])
    {
      Console.WriteLine("Arg: {0}", arg);
    }

    foreach (Type type in (Type[])msg.Properties["__MethodSignature"])
    {
      Console.WriteLine("Signature: {0}", type);
    }


    return null;
  }
}

 

运行测试,果然看到了期望的结果:


Arg
: 3
Arg: 5
Signature: System.Int32
Signature: System.Int32

 

也就是说,在传入的IMessage中的数据字典中,__Args这一项中包含了所有传入参数的数值序列,而__MethodSignature则是对应的参数的类型序列(method signature在很多书中被译为方法签名,其实它的定义很简单:就是方法的参数列表中参数类型的序列,它最初的用途大概是用来结合方法名称识别特定的方法重载的)。

 

现在我们希望能够让测试代码再次正确的运行,这就需要我们能够从Invoke()方法返回时将该方法在真实对象上调用时同样的返回值返回给调用代码。我们写return 8……恐怕不行。因为Invoke()方法的返回类型也是一个IMessage,也就是说透明代理希望我们把返回的结果也包装在一个消息对象中返回——可是我怎么知道如何包装这么个数据字典呢?还好,发现一个叫ReturnMessage的类,看样子是干这个的。我们可以构造一个ReturnMessage的实例,让它将我们的返回值通过透明代理带回调用代码去!这个类有两个看上去截然不同的构造器(自己翻一下文档啦),一个是用来处理正常返回情况的(就是带ret参数的这个,它应该就是实际的返回值喽),而另一个则可以处理异常情况(就是那个e)。outArgs/outArgsCount不用说,应该是用来返回输出参数的。LogicalCallContext先不管它,先给个null试试吧!那IMethodCallMessage是什么?顾名思义,一个代表着方法调用的消息——原来,它就是一个源于IMessage(更确切的说——IMethodMessage)的接口,一看定义就明白,原来它将IMessage中的属性字典中的很多项用属性和方法的形式发布出来了,这样我们就可以更直观的访问传入的代表着方法调用的消息了。那么现在我们就让Invoke()中返回测试代码所期待的“正确结果”吧:

 

public class MyRealProxy: RealProxy
{
  public override IMessage Invoke(IMessage msg)
  {
   
    IMethodCallMessage callMsg = msg as IMethodCallMessage;

    int x = (int)callMsg.InArgs[0];
    int y = (int)callMsg.InArgs[1];
    int result = x+y;

    return new ReturnMessage(
result, null, 0, null, callMsg);
  }
}

 

编译并运行测试,爽——又绿了。咦,等等——好像现在我们算这个加法的时候并没用到Calculator这个干活的类啊?幸亏只是个加法,要是算个圆周率什么的就有好看了!你说了,这个还不好办,我创建一个Calculator来干活不就行了:

 

public class MyRealProxy: RealProxy
{
  public override IMessage Invoke(IMessage msg)
  {
   
    Calculator calc = Calculator.CreateInstance();
    int result = calc.Add(x, y);

   
  }
}

 

嗯?好像不行——肯定要死循环了,因为我们正在处理这个Add()方法调用呢,而CreateInstance()返回的实例还会是一个转发给这个真实代理的透明代理(虽然不是同一个实例)。这里如果不用透明代理就好了,我们需要的其实是真正干活的那个核心实现,这个好办,在构造真实代理的时候就传进来一个能干活的真实对象不就行啦:

 

public class Calculator: MarshalByRefObject
{
  public static Calculator CreateInstance()
  {
    Calculator realCalculator = new Calculator();
    RealProxy realProxy = new MyRealProxy(realCalculator);
    object transparentProxy = realProxy.GetTransparentProxy();
    return (Calculator)transparentProxy;
  }
}

 

这样的话我们得为MyRealProxy添加一个相应的构造器:

 

public class MyRealProxy: RealProxy
{
 
  private MarshalByRefObject target;

  public MyRealProxy(MarshalByRefObject target): base(target.GetType())
  {
    this.target = target;
  }

}

 

经过这样的改造,当我们再遇到Invoke()调用时,应该就可以访问到最开始传入的真正干活的Calculator对象并用它进行真正的操作了吧?可是……等等,我们该怎么把从透明代理那里得到的方法调用消息转发给这个对象呢?我们可不会操作堆栈呀!总不能写int result = ((Calculator)target).Add(x, y)吧!我们可是打算让这个真实代理为50个组件的1000个方法服务呢啊……我们的答案就是RemotingServices.ExecuteMessage()方法。RemotingServices是一个位于System.Runtime.Remoting名称空间中的工具类,它提供了很多实用的辅助方法用来帮助我们实现包括真实代理在内的很多底层类。其ExecuteMessage()方法用法超级简单,作用也一目了然——就是将方法调用消息转发给指定的目标对象上执行,最后将返回的结果再打包成消息返回。有了它的帮助,我们就不用再自己去碰那些InArgs什么的啦:

 

public class MyRealProxy: RealProxy
{
  public override IMessage Invoke(IMessage msg)
  {
   
    IMethodCallMessage callMsg = msg as IMethodCallMessage;

    IMessage returnMsg = RemotingServices.ExecuteMessage(target, callMsg);

    return returnMsg;
  }
}

 

编译并运行测试,可以发现一切正常。不过背后发生的事情才是最重要的:我们已经拥有了拦截任意MarshalByRefObject对象上任意方法的基本手段,那么剩下来的事情就简单多了!这里插一句,就像你已经知道的这样,透明代理是负责把方法调用的堆栈转换成消息并转发给一开始构造它的那个真实代理的Invoke()方法,可又是谁把方法返回的消息转换回堆栈并发送给真实对象的呢?又是谁把真实对象方法执行的结果从堆栈上再次打包为消息返回Invoke()方法的呢?这个家伙其实是StackBuilderSink,我们后面还会再提到它的,现在先打个照面的说。

 

回过头来仔细观察上面的代码,可以发现真正对核心真实对象(target)方法的执行就是发生在调用RemotingServicesExecuteMessage()方法之时。在它之前,我们可以通过callMsg取得(甚至修改)所有的关于方法调用的信息(就是AOP基本操作之pre-processing啦);在它之后,我们又可以通过returnMsg取得(甚至修改)所有关于方法返回的信息(也就是AOP基本操作之post-processing啦!还记得两种不同的情形吗——正常返回与抛出异常)——这里提示你去看一下与IMethodCallMessage对应的IMethodReturnMessage接口,我们从ExecuteMessage()方法得到的IMessage是可以转换为这个接口进行直观访问的。

 

好了,现在就请您按照目前的得到的信息自己写一个真实代理类,比如说就叫LoggingProxy吧。你可以让这个真实代理为所代理的对象上的每一次方法调用都打印出一行记录,比如:

 

[2004-02-16 12:34:56] Calculator.Add(3, 5) -(37ns)-> 8

 

应该不难吧?写好了再继续往下看啦……

 

现在我们已经有了两个真实代理类(什么?还没写好?不要偷懒啊:),一个是MyRealProxy,它简单的在方法调用进入时输出msg.Properties中的内容;一个是你刚写的LoggingProxy,它应该可以记录下每次方法调用的日志信息。现在我们希望能够像组合DECORATOR那样将这两个真实代理所提供的功能叠加组合起来,我们该怎么办呢?有了前一篇文字的知识和思路,我们首先可以再次尝试使用DECORATOR模式将几个真实代理所构造的透明代理彼此连接起来(啊?可以吗?——怎么不可以?要记住透明代理的魔力——它对于调用代码来说跟你所代理的类型的实例毫无二致)。就像这样:

 

public class Calculator: MarshalByRefObject
{
  public static Calculator CreateInstance()
  {
    Calculator realCalculator = new Calculator();
    RealProxy realProxy = new MyRealProxy(realCalculator);

    Calculator
fakeCalculator = (Calculator)realProxy.GetTransparentProxy();
    RealProxy loggingProxy = new LoggingProxy(
fakeCalculator);

    Calculator result = (Calculator)loggingProxy.GetTransparentProxy();

    return result;
  }
 
}

 

现在CreateInstance()返回的透明代理其实是由LoggingProxy的实例构造的,而该实例内部真正干活的对象又是由之前MyRealProxy构造并返回的透明代理,最终干活的对象则是最开始构造的realCalculator。当调用代码向这个透明代理发出方法调用时,loggingProxy的透明代理首先将堆栈上的信息打包传给LoggingProxyInvoke()方法;然后你在LoggingProxyInvoke()内调用RemotingServices.ExecuteMessage()将这个消息(通过StackBuilderSink转换回堆栈——还记得前面提到的这位仁兄吗?:)转发给目标对象——也就是由MyRealProxy构造的那个透明代理——然而这次转发将是一次由消息到堆栈再到消息的过程(另外别忘了,无论如何最终从Invoke()方法返回的消息还会被透明代理转换回基于堆栈的方法调用结果的——调用代码对这一路上发生的这一串儿事情真是毫不知情)!

 

一串儿事情?这不禁让我们想起了另一个设计模式,也就是CHAIN OF RESPONSIBILITY(职责链)。我们可以把一系列对方法调用消息感兴趣的处理代码封装到一个个独立的、高度内聚的消息处理对象中,并通过将其串接成链表形成一个职责链,让方法调用消息沿着这条职责链一路走下去,直到抵达真实对象——而后方法返回消息再沿原路返回依次途径职责链上的每一个参与者。通过这样的机制,我们避免了反复在堆栈与消息之间进行转换所带来的额外开销,从而把所有的方法拦截处理活动在一个统一的基于消息的世界中搞定——也就是说,我们需要一个链式反应的场所——显然,它也应该是一个真实代理,我们不妨把它叫做MessageChainProxy,即消息链代理吧:

 

public class MessageChainProxy: RealProxy
{
  private MarshalByRefObject target;

  public MessageChainProxy(MarshalByRefObject target): base(target.GetType())
  {
    this.target = target;
  }

  public override IMessage Invoke(IMessage msg)
  {
    return null;
  }
}

 

怎么样,这六行实现一个不干活的RealProxy的骨架代码现在你也已经可以轻车熟路的写出来了吧?现在我们开始编写实质性的代码。正如前面所说,如果有多个需要串连起来的对方法调用消息进行链式处理的代码,我们希望将它们封装起来,成为一个消息处理与转发器——正好.NET Remoting里面已经定义了这样一个语义,我们不妨直接拿来用用,这就是IMessageSink(消息接收器)接口(同样在System.Runtime.Remoting.Messaging名称空间中):

 

public interface IMessageSink
{
  IMessageSink NextSink { get; }
  IMessage SyncProcessMessage(IMessage msg);
  IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink);
}

 

仔细一看其实这个接口很简单:NextSink顾名思义,肯定是指向消息处理链中的下一个接受器喽;SyncProcessMessage肯定就是真正处理方法消息的方法啦(这一个是用于同步场合,而另一个A开头的则是用于异步场合——异步方法调用在.NET中还算是一级支持的)。考虑到我们可能将需要编写越来越多的消息处理器,不妨为这个接口提供一个基础实现吧:

 

public abstract class MessageSinkBase: IMessageSink
{
  private readonly IMessageSink nextSink;

  public MessageSinkBase(IMessageSink nextSink)
  {
    this.nextSink = nextSink;
  }

  public IMessageSink NextSink
  {
    get { return nextSink; }
  }

  public abstract IMessage SyncProcessMessage(IMessage msg);

  public virtual IMessageCtrl AsyncProcessMessage(IMessage msg, IMesssageSink replySink)
  {
    return nextSink.AsyncProcessMessage(msg, replySink);
  }
}

 

在这个抽象的基类中,我们实现了IMessageSink接口中的两个方法:NextSinkgetter因为觉得不太可能需要灵活改写,所以实现为默认的非虚方法了(嗯?怎么成方法了,不是属性吗?别忘了属性其实就是一对或一个方法噢!);SyncProcessMessage正是我们要求派生类必须提供且没有默认实现的逻辑(这不是个TEMPLATE METHOD嘛),所以声明为抽象的还是很自然的吧;而AsyncProcessMessage呢,考虑到不是所有的消息接收器都支持异步方法的拦截处理,不如提供一个默认的实现将其转发到nextSink上去(或者实现为抛出NotSupportedException,这也是很常见的做法),也就是说派生类可以根据需要可选实现或覆盖这个逻辑。现在我们就可以将原来的两个RealProxy中的Invoke()方法提取出来作为IMessageSink中的SyncProcessMessage()来实现啦,因为实在没什么可说的,这里就不再浪费网络带宽喽(%$^%%$#)。我们还是来看MessageChainProxy如何实现职责链吧,因为这一块也并不是本文着重要讲的话题,所以我也就不再推导了,相信你可以很容易的看懂这部分代码:

 

public class MessageChainProxy: RealProxy
{
  private MarshalByRefObject target;
  private IMessageSink headSink;

  public MessageChainProxy(MarshalByRefObject target): base(target.GetType())
  {
    this.target = target;
    this.headSink = new TerminatorSink(target);
  }

  public override IMessage Invoke(IMessage msg)
  {
    return headSink.SyncProcessMessage(msg);
  }

  public void AppendSinkType(Type sinkType)
  {
    object[] ctorArgs = new object[] { headSink };
    IMessageSink newSink = (MessageSinkBase)Activator.CreateInstance(sinkType, ctorArgs);
    headSink = newSink;
  }

}

 

代码中,headSink就是整个消息处理链的头结点,而整个调用链的驱动乃是由每个sink在自己的xxxProcessMessage()方法中显式调用NextSink.xxxProcessMessage(msg)来形成的(这与我们在DECORATOR模式中每个DECORATOR中调用Decoratee.Method()是异曲同工的)。值得注意的是一个边界情况,即MessageChainProxy在刚被构造完成后,即没有调用任何AppendSinkType()添加处理节点前,我们希望它也能正常工作(就是什么都不用做只要别抛出空引用异常就好),所以我们要引入一个TerminatorSink的概念——这个和SCSI设备链中的terminator概念是一样的,就是一个终结器。我们将其实现如下(注意看一下粗体的部分就行了,我就不再详细分析了——如果你这里还是看不明白,那我文章算是白写了:)

 

private class TerminatorSink: IMessageSink
{
  private MarshalByRefObject target;

  public TerminatorSink(MarshalByRefObject target)
  {
    this.target = target;
  }

  IMessageSink IMessageSink.NextSink
  {
    get { return null; }
  }

  IMessage IMessageSink.SyncProcessMessage(IMessage msg)
  {
    return RemotingServices.ExecuteMessage(target, msg as IMethodCallMessage);
  }

  IMessage IMessageSink.AsyncProcessMessage(IMessage msg, IMessageSink replySink)
  {
    throw new NotSupportedException();
  }
}

 

想想看,道理其实很简单,这也同样是一种很常见的面向对象设计模式,有的地方管它叫做NULL OBJECT,总之就是为可能出现空引用的场合也提供一种替代对象,从而简化复杂的条件判断逻辑——写到这里恐怕又有高手要抗议说过度设计了,其实偶就是多给大家一种解决问题的思路而已,具体怎么选择还是要放到具体的环境中考虑——不过多一种选择总比没得选择要好吧(何况这里提出这个概念也是为下篇中的一些内容作铺垫呢:)

 

好了继续往下写……因为这个TerminatorSink仅在我们自己的MessageChainProxy里面用到,所以将其定义为私有的内嵌类是再合适不过的了:

 

public class MessageChainProxy: RealProxy
{
  private class TerminatorSink: IMessageSink
  {
   
  }

 
}

 

现在我们就可以在CalculatorFACTORY METHOD里面使用这个具有可扩展能力的消息处理链式代理类了:

 

public class Calculator: MarshalByRefObject
{
  public static Calculator CreateInstance()
  {
    Calculator realCalculator = new Calculator();

    MessageChainProxy chainedProxy = new MessageChainProxy(realCalculator);

    chainedProxy.AppendSinkType(typeof(MyMessageSink1));
    chainedProxy.AppendSinkType(typeof(
MyMessageSink2));
   


    return (Calculator)chainedProxy.GetTransparentProxy();
  }
 
}

 

这里你需要注意一下,由于是一个有序的调用链,因此添加消息处理器结点的顺序对于最终的执行逻辑是有很大影响的。比如说你有两个消息处理器:一个是做方法日志的,第一个添加到处理链中(其NextSink将指向TerminatorSink);第二个是做访问控制的(就是根据代码调用者的安全身份和权限决定是否允许执行该方法),在其后添加到处理链中(NextSink即指向前面的方法日志处理器)。这时候,当调用代码发出一个对目标对象上方法的调用时,该基于堆栈的调用首先被CLR的透明代理截获并转换为方法调用消息,随后被发送给对应的真实代理也就是我们的MessageChainProxyInvoke()方法处理,而该方法立即把该消息传递给headSink也就是最后被添加到处理链上的访问控制处理器的SyncProcessMessage()方法上。此时,如果访问控制器允许方法被继续执行下去,则同样的方法调用消息被转发给NextSink所指向的方法日志处理器处理,最终方法消息到达TerminatorSink并被转发给RemotingServicesExecuteMessage()方法——还记得前面讲过的这个方法的执行逻辑吧:一个透明代理的“反透明代理”也就是StackBuilderSink(你看,它的名称已经暗示了它其实也是一个IMessageSink!)最终利用传入的方法调用消息重建调用堆栈,并发往真正干活的我们的Calculator类……但是如果在访问控制器这一关没有过去,而是被抛出了安全异常的话(要记得用ReturnMessage构造一个代表方法调用异常的返回消息噢!不然的话透明代理也就不再透明了……),显然方法日志这一环节根本就不会被执行到了(更甭说随后的TerminatorSinkStackBuilderSink了,当然我们对权限控制完全无知的Calculator也就永远不会看到这个越权的方法调用了)。所以说,如果你希望记录下所有的方法调用请求,就应该把方法日志处理器放到权限控制的前面来,也就是说——在添加完访问控制器以后再添加日志处理器就行了。这个道理还是比较简单的,和前面说的DECORATOR模式的实现是一样的——我这里再捋一遍也就当作本篇文字的再回首吧。

 

行文至此(好拽),我们已经对基于TP/RPIMessage的方法拦截机制有了初步的了解,利用这些知识应该已经可以解决很多问题了。那么前文中描写的基于DECORATOR的机制就没有任何用场了吗?其实不然。这两种机制的截然不同本身也暗示了它们将在不同的场合和情况下发挥不同的优势。首先,不可否认的是,DECORATOR基于堆栈的方法转发机制肯定要比基于IMessage的转换传递机制效率高的多,当然对于很多涉及远程方法调用的企业级应用或者在本地运行的桌面应用而言损失的性能可能算不上什么主要矛盾,然而你仍然不得不时刻记着这个潜在的陷阱——尤其是在选择技术的起始阶段。然而,通过基于透明代理的机制我们可以把一些与具体对象类型甚至方法都无关的通用逻辑集中在一个真实代理中实现,并利用对象构建模式将其动态的(即运行时)“缝合”到目标对象的每一个方法上去,这个机制对于为大量对象添加n通用行为的场合是很有诱惑力的——甚至很容易让你为之妥协因此可能损失的些许性能!不过,既然是通用性很强,那么对于需要针对性的场合,比如说根据方法传入的参数的值来具体判断访问权限等场合,还是基于DECORATOR接口实现的方法转发更为灵活——让一个为通用性而设计的机制去处理针对性的场合本身肯定会得不偿失的。因此,最好的结局恐怕还是需要把两种机制无缝的整合在一起,也就是说把一些需要针对性处理的功能叠加通过DECORATOR的机制施加到核心对象上,而把一些通用的功能利用透明代理的魔力作用到对象上——这个机制事实上已经在我们当前的项目开发中设计并采用了,确实给我们的项目开发带来了很多的好处(减少数千行代码、节省很多的人月、提交易于维护且灵活的软件系统——这还不够吗?:)

 

最后,把这部分内容相关的一些小技巧贴着这里,也许对你有用的说:

  • IMethodMessage这个接口中引入了MethodBase这个属性,透过它你可以进入Reflection的世界检视方法上的类型信息包括自定义属性。用它你可以为真实代理的Invoke()方法提供参数,从而增加系统的灵活——记住:这只是增加一种灵活性的机制,并没有取代其他的机制——你还是可以从配置文件中读入有关的参数化信息,对吧progame?:)
  • 真实代理不仅仅可以为MarshalByRefObject构造透明代理,它也可以给一个接口构造透明代理。所以你完全可以用一个真实代理为很多不同的接口提供基于方法调用消息的实现——就像我们在ElegantDAL里面使用的方法一样,只要定义接口、获取透明代理,不需要提供接口的实现类,就可以完成原本需要一一实现的繁琐功能。具体做法请参考我们一开始直接用ReturnMessage返回执行结果的实现,原理是一样的。
  • 不过当一个透明代理所代理的目标为接口时,你将不能再用RemotingServices.ExecuteMessage()将方法调用消息转发给它了。这里你需要这两个帮手:RemotingServices.IsTransparentProxy()RemotingServices.GetRealProxy()。一旦你手里有了真实代理,怎么样转发消息就不言而喻了吧?
  • 更多的小技巧还是留给你自己在实践中摸索吧,我都说了就没意思了!:)

到这里,本篇的内容已经基本完成,但是挑剔的读者肯定还是有很多不爽的感觉,比如说:

  • 想拦截构造器调用,可是真实对象已经先于真实代理创建好了,怎么办呢?
  • 要想用TP/RP包装对象,就要写FACTORY METHOD封装整个的构造过程,能不能直接用更直观的new命令来直接创建一个代理好的对象呢?
  • 如果你在Calculator类的某些方法中将代表对象实例的this传递给对象外部的时候,这个引用是直接指向真实对象的引用!所以你精心设置的关键的比如说访问控制器等机制就将被绕过了!怎么样才能保证外部调用总是经过我设置的重重关卡呢?
  • 更多的不爽请你通过反馈发送给我 :)

其实这些内容本来也是计划在本文中一并完成的,可是篇幅所限(借口!)我想还是留到下一篇文字中继续分析并解决吧!其间还是一如既往的请你把与本文相关的意见、建议、收获、疑惑等等通过留言或者邮件告诉我,您们的支持是我写作的最大动力!再次谢谢大家的阅读!:)

posted on 2004-02-20 23:29:00 by jgtm2000  评论(44) 阅读(13283)

.NET Quick Tip: Runtime Type Conversion for Enumeration

先来看这段NUnit测试代码,我们希望用反射机制在运行时访问一个对象的枚举类型的域或属性:

 

[TestFixture]
public class PaymentInfo
{
  public enum PaymentType
  {
    Cash, CreditCard, Check
  }

  public PaymentType Type;

  public void Test()
  {
    PaymentInfo payment = new PaymentInfo();
    payment.Type = PaymentType.Cash;

    System.Reflection.FieldInfo enumField = GetType().GetField("Type");

    int paymentTypeInt32;

    paymentTypeInt32 = (int)enumField.GetValue(payment);
    Assert.AreEqual((int)PaymentType.Cash, paymentTypeInt32);

    enumField.
SetValue(payment, paymentTypeInt32);
    Assert.AreEqual(PaymentType.Cash, payment.Type);
  }
}

 

实际上运行测试时发现在标红的这行上抛出一个异常:“对象类型无法转换为目标类型”。究其原因,原来是因为CLR的反射机制不允许枚举类型与整数类型之间隐式转换。不过C#编译器还是允许我们通过强制类型转换的语法来进行两者间的显式转换。

 

在这个测试中,使之通过的办法其实非常简单:把划线部分强制转换为枚举类型即可,如:(PaymentType)paymentTypeInt32。可问题是:在运行时如何动态转换类型呢?比如说我在写ElegantDAL的时候,需要将从数据库读出的一个类型为int的数值写入到要返回的对象的一个枚举型字段中,此时我只有fieldInfocolumnValueresultObject,然而写成fieldInfo.SetValue(resultObject, columnValue)就会出现前面提到的错误,可是我又只有一个运行时的Type信息(fieldInfo.FieldType),我又不能写成fieldInfo.SetValue(resultObject, (fieldInfo.FieldType)columnValue)……

 

只好将这种情况列为一个特例处理,而我们的救兵则是Enum.ToObject()方法——你知道有更好的方法解决这个问题吗?

 

BTW: 很多朋友写信来问偶承诺的下一篇文章什么时候才能搞定,其实我也不想拖,可最近工作确实比较紧张,而我的最佳写作时间又都在深更半夜(之前还要热身进入状态),所以迟迟不能结稿。目前其实已经写了好多(已经和第一篇长度差不多了),可是发现要写的内容还太多(也许是我写得太细了,因为我的目标不仅是讲how,更多的是whatwhy),所以现在决定把本篇文字再细分为两部分(上篇只讲TP/RP+MBR对象;把CBO相关的内容放到下篇再写),以便能够尽快让大家看到新的内容——嗯,争取周五release吧。至于还有朋友希望我能够深入写些关于ElegantDAL的设计与实现细节,这个留待稍后吧,正好我们将要整理的项目文档中也要有这一块内容(现在在我们这个企业级项目中用得还真挺好),到时候一起写啦(希望能够和canyue尽快完成第三版本的重写工作,目前使用的是第二版)。

posted on 2004-02-18 20:16:00 by jgtm2000  评论(5) 阅读(6642)

XP: To be a Good Programmer, You'd Find a Pair Programmer First!

 

昨天,我第一次在实际开发工作中体验到了结对开发的魔力(以前也就是两个人一起研究问题而已),那确实是一种令人神怡且有效的工作方式——至少it works extremely well for me!

 

在下午三四个小时的开发工作中,我和我的搭档一起解决了已经困扰了我很久的几个问题,并令人激动不已的在NUnit中点绿了单元测试中所有的测试项(就这几个问题我已经拖了半个月了还没能解决)——这意味着我可以放心的开始对代码进行重构(而不是重写)了(本来一直没有信心这样做,因为到处都有红灯,而每一个红灯都牵扯到很多的问题)……体会一:结对编程消除了最影响开发人员情绪的孤独感,进而重新迸发工作激情,大大提升工作效率和质量。

 

其他同事都下班以后,我们简直是进入了一种turbo的开发状态——我们先是一起总结了前面开发中最不爽的几个环节,并一致认为一定有好办法能够让开发变得更简单一些——于是我的灵感突然涌出,我告诉他“我将把让我们的开发变得繁琐的这些类都删掉,用一个custom proxy搞定所有的类”!“听上去不错,可是……有这么好的事吗?”“看我的——你就帮我瞧着那些绿灯吧!”当然,我在下手之前还是像唐僧一样把改造的思路说了几遍,当我确信搭档已经明白我的意图后,我开始动手了:三下五除二,我先把一个private成员提为internal(本来它只是被自己的一系列public gateway methods访问的,现在我要让它能被custom proxy使用),并马上写出了这个custom proxy,用它替代了以前那些繁琐的method gateway和那个接口实现类,最后,我到组件工厂中把实现接口的类换为由custom proxy构造的transparent proxy——NUnit.Run! Bingo! We got it! 体会二:两个人的智慧确实大于一个人,尤其当两个人从两个方向共同考虑同一个问题的时候——当然,彼此持续、默契的交流是必不可少的。

 

现在回头想想,这一下午的进展真的是匪夷所思,我又一次亲身体会到了结对编程的魔力和乐趣——当然,坐在我对面的项目经理估计也体验了他工作以来强度最大、持续时间最长的人造噪音。不过我想说的不是sorry,而是thanks!谢谢你给我们这样的机会,这是在其他团队中难以想象的事情。我相信你也将亲眼看到我曾向你描述的关于结对编程的种种好处。另外,thanks to my partner canyue!和你一起工作真的很开心,我在你谨慎的约束下变得更专注也更有效率,而你也能在看我开发的时候学到更多的东西。Cool!

 

如果你也有类似的敏捷开发经历,不妨写在评论中与我们分享——无论是成功的经历还是失败的经历,我想都会很有参考价值。其实关于敏捷方法论(尤其是其中最著名的极限编程技术),我也有很多实践经验和想法,希望以后有时间能够在这方面也写些心得体会吧——不过当务之急,还是先把腾苦不堪的颈椎疼痛消灭掉吧!现在开车都成问题了……

posted on 2004-02-07 16:44:00 by jgtm2000  评论(18) 阅读(5914)

动态属性依然是类的一部分?

动态属性依然是类的一部分?——I really do NOT think so!
http://blog.joycode.com/microhelper/posts/12394.aspx#12408

不可否认attribute的强大 但它必须依赖程序而存在的事实就注定了它的脆弱?——What do you think?
http://blog.joycode.com/jgtm2000/posts/10064.aspx#10220

“具体情况、具体分析”——就算所有的技术都是死的,人还是活的……

posted on 2004-02-05 14:12:00 by jgtm2000  评论(35) 阅读(7134)

A Taste of AOP from Solving Problems with OOP and Design Patterns (Part I)

摘要:本文通过一个高度简化的示例,逐步的揭示在面向对象设计与开发中面临的若干问题,并导出软件开发对AOP(即面向方面编程)思想的需求。这是文章的第一部分,主要利用面向对象编程技术与设计模式给出解决问题的一个思路,并分析该实现面临的问题,以此形成对面向方面编程技术的直观需求。

 

欢迎大家提出自己的观点、意见和建议,文章的下半部分正在写作中。谢谢!:)

 

http://blog.joycode.com/jgtm2000/posts/12103.aspx

posted on 2004-02-02 19:15:00 by jgtm2000  评论(1) 阅读(4481)

A Taste of AOP from Solving Problems with OOP and Design Patterns (Part I)

 

在展开问题之前我们首先设定一个例子,在这个示例中我将使用尽可能简单的逻辑实现所有功能需求,这将更突出我们所要解决的核心问题。例子是一个简单计算器类:

 

public class Calculator
{
  public int Add(int x, int y) { return x + y; }
}

 

测试代码如下(你可以使用NUnit与我们一起完成对这个例子的研究):

 

public void Test()
{
  Calculator calculator = new Calculator();

  Assert.IsNotNull(calculator);
  Assert.AreEqual(5, calculator.Add(2,3));
}

 

这个类再简单不过了,不过若你将它想象为一个可能更复杂的业务处理类的时候,你将面临除了核心功能实现之外的更多处理细节,比如说:权限控制、审计日志、性能监测、缓冲处理、事务环境等等。为简单起见,我们首先为该类增加记录日志的功能,该功能要求将对每个方法的调用和处理结果输出到Console中,如下:

 

public class Calculator
{
  public int Add(int x, int y)
  {
    Console.Write("Add({0},{1})", x, y);
    int result = x + y;
    Console.WriteLine(" = {0}", result);
    return result;
  }
}

 

再简单不过了,对吧?现在我们需要为该方法实现性能监测,如下:

 

public class Calculator
{
  public int Add(int x, int y)
  {
    Console.Write("Add({0},{1})", x, y);
    DateTime startTime = PreciseTimer.Now;
    int result = x + y;
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    Console.Write(" [{0}] ", elapsedTime);

    Console.WriteLine(" = {0}", result);
    return result;
  }
}

 

这里暂时不要去管PreciseTimer这个类,它只是一个用来计时的工具而已,跟我们的核心问题没什么大关系。此时你已经感觉到,虽然我们实现了所需的功能,但是在一个方法中堆叠了处理各类事宜(Aspect! I heard you! :)的不同代码。虽然在这个简单例子中不会感觉有什么不爽,但是请你想象一下如果我们将为该类添加第二个方法时会发生什么事情:

 

public class Calculator
{
  public int Add(int x, int y)
  {
    Console.Write("Add({0},{1})", x, y);
    DateTime startTime = PreciseTimer.Now;
    int result = x + y;
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    Console.Write(" [{0}] ", elapsedTime);
    Console.WriteLine(" = {0}", result);
    return result;
  }

  public int Subtract(int x, int y)
  {
    Console.Write("Subtract({0},{1})", x, y);
    DateTime startTime = PreciseTimer.Now;
    int result = x - y;
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    Console.Write(" [{0}] ", elapsedTime);

    Console.WriteLine(" = {0}", result);
    return result;
  }

}

 

此时的单元测试代码补充如下:

 

public void Test()
{
  ICalculator calculator = new Calculator();

  Assert.IsNotNull(calculator);
  Assert.AreEqual(5, calculator.Add(2,3));
  Assert.AreEqual(5, calculator.Subtract(8,3));
}

 

在两个方法中已经明显出现重复代码了,这可不是一个好的smell——想想一下如果我们的计算器有10个方法呢?如果我们还有类似于计算器类的另外数十个类呢?如果我们还有更多的方法级功能要实现呢(权限控制、事务管理……)?在企业级应用开发中,这可是一个经常会遇的问题。为清楚起见,我们将问题分解成两部分,首要的问题是代码职责混淆,其次则是同样的代码逻辑反复多次——这些问题都将导致开发管理、代码编写与维护的各种困难。

 

为了解决代码职责混淆的问题,我们首先考虑的是使用DECORATOR设计模式将不同职责的代码分解到若干个DECORATOR中,并继而使用一种对象构造模式(如ABSTRACT FACTORYFACTORY METHODBUILDER等等,都可以)在运行时将DECORATOR与核心实现组装起来。为了实现DECORATOR模式,我们首先需要使用Extract Interface的重构技术提取接口,如下:

 

public interface ICalculator
{
  int Add(int x, int y);
  int Subtract(int x, int y);
}


public class Calculator: ICalculator
{
  public int Add(int x, int y)
  {
    Console.Write("Add({0},{1})", x, y);
    DateTime startTime = PreciseTimer.Now;
    int result = x + y;
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    Console.Write(" [{0}] ", elapsedTime);
    Console.WriteLine(" = {0}", result);
    return result;
  }

  public int Subtract(int x, int y)
  {
    Console.Write("Subtract({0},{1})", x, y);
    DateTime startTime = PreciseTimer.Now;
    int result = x - y;
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    Console.Write(" [{0}] ", elapsedTime);
    Console.WriteLine(" = {0}", result);
    return result;
  } 
}

 

测试代码相应修改如下:

 

public void Test()
{
  ICalculator calculator = new Calculator();

  Assert.IsNotNull(calculator);
  Assert.AreEqual(5, calculator.Add(2,3));
  Assert.AreEqual(5, calculator.Subtract(8,3));
}

 

运行测试代码可以确信现在Calculator的行为未受到任何影响(这是正确使用重构技术的重要标志噢),那么我们开始剥离负责性能监测职责的代码并将其作为一个DECORATOR实现:

 

public class Calculator: ICalculator
{
  public int Add(int x, int y)
  {
    Console.Write("Add({0},{1})", x, y);
    DateTime startTime = PreciseTimer.Now;
    int result = x + y;
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    Console.Write(" [{0}] ", elapsedTime);

    Console.WriteLine(" = {0}", result);
    return result;
  }

  public int Subtract(int x, int y)
  {
    Console.Write("Subtract({0},{1})", x, y);
    DateTime startTime = PreciseTimer.Now;
    int result = x - y;
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    Console.Write(" [{0}] ", elapsedTime);

    Console.WriteLine(" = {0}", result);
    return result;
  } 
}

 

public class CalculatorTimer: ICalculator
{
  public int Add(int x, int y)
  {
    DateTime startTime = PreciseTimer.Now;
    int result =
Decoratee.Add(x, y);
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    return result;
  }

  public int Subtract(int x, int y)
  {
    DateTime startTime = PreciseTimer.Now;
    int result =
Decoratee.Subtract(x, y);
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    return result;
  }
}

 

可以看到,在负责性能监测(在这个简单例子中我们还不如就叫它timer吧:)的DECORATOR中,代码只关注计时的功能,而将核心的工作交由decoratee(自造词,别介意——caller/calleetester/testeeassigner/assigneebomber/bombee……:)去处理。这里的decoratee也是一个ICalculator类型的接口引用,我们需要在构造decorator的时候为其设置好decoratee,这个工作将在对象工厂中实现。这里我一次到位的为所有可能的DECORATOR实现一个基类,该类实现一个新的接口ICalculatorDecorator,在这个接口中我们声明一个支持读写decoratee的语义,如下:

 

public interface ICalculatorDecorator
{
  void InitDecorator(ICalculator decoratee);
  ICalculator Decoratee { get; }
}

 

然后我们为所有的Decorator提供该接口的一个基本实现:

 

public abstract class CalculatorDecorator: ICalculatorDecorator
{
  private ICalculator decoratee;


  void ICalculatorDecorator.InitDecorator(ICalculator decoratee)
  {
    this.decoratee = decoratee;
  }

  // FIXED: to use implicit interface implementation instead explicit way
  // so that derived classes could access this property. Thanks
greatqn!
  ICalculator ICalculatorDecorator.Decoratee
  public ICalculator Decoratee
  {
    get { return this.decoratee; }
  }
}

 

再由此派生出之前的CalculatorTimer

 

public class CalculatorTimer: CalculatorDecorator, ICalculator
{
  public int Add(int x, int y)
  {
    DateTime startTime = PreciseTimer.Now;
    int result = Decoratee.Add(x, y);
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    return result;
  }

  public int Subtract(int x, int y)
  {
    DateTime startTime = PreciseTimer.Now;
    int result = Decoratee.Subtract(x, y);
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    return result;
  }
}

 

为了将这个负责性能监测职能的DECORATOR与其被装饰者(decoratee)组合起来,我们需要一个对象工厂类(它同时可以将调用代码逻辑与对象创建逻辑解耦):

 

public class CalculatorFactory
{
  private CalculatorFactory() {}

 
private static Type[] GetObjectGraph()
  {
    ArrayList objectGraphTypes = new ArrayList();

    // Add decoratee as 1st type to be created

    objectGraphTypes.Add(typeof(Calculator));

    // and then add all decorators
    objectGraphTypes.Add(typeof(CalculatorTimer));

    return (Type[])objectGraphTypes.ToArray(typeof(Type));

  }

  public static ICalculator CreateInstance()
  {
    Type[] objectGraphTypes = GetObjectGraph();


    ICalculator result = null;

    foreach (Type calcType in objectGraphTypes)
    {
      ICalculator calcImpl = (ICalculator)Activator.CreateInstance(calcType);

      if (calcImpl is ICalculatorDecorator)
      {
        ((ICalculatorDecorator)calcImpl).InitDecorator(result);
      }

      result = calcImpl;
    }

    return result;

  }
}

 

相应修改单元测试使其使用这个工厂的CreateInstance()方法(这实际是设计模式中所谓的FACTORY METHOD),如下:

 

public void Test()
{
  ICalculator calculator = CalculatorFactory.CreateInstance();

  Assert.IsNotNull(calculator);
  Assert.AreEqual(5, calculator.Add(2,3));
  Assert.AreEqual(5, calculator.Subtract(8,3));
}

 

在这个类里面我们提供了一个静态方法CreateInstance()用来返回ICalculator的一个实现。在这个实现逻辑中,我们首先构造一个对象组装图(object graph),其实就是一个类型列表,按照从内到外的顺序依次添加若干ICalculatorICalculatorDecorator的实现类型。接下来,一个foreach循环按照构造列表依次创建每一个类型,并在创建后判断新创建的类型是否是一个ICalculatorDecorator,如果是的话,我们将已然创建好的最后一个ICalculator实现(即result)作为decoratee赋予它以实现对象组合。循环完成时,result所引用的ICalculator实现就是最外面的一个decorator,而整个调用链其实是通过每一个DECORATOR类的方法中的Decoratee.XXX()来构成的。

 

有了这个改进的对象结构,我们可以很容易的添加新的DECORATOR为对象增加新的职责,如将之前的日志记录逻辑实现如下:

 

public class CalculatorLogger: CalculatorDecorator, ICalculator
{
  public int Add(int x, int y)
  {
    Console.Write("Add({0}, {1})", x, y);
    int result = Decoratee.Add(x, y);
    Console.WriteLine(" = {0}", result);
    return result;
  }

  public int Subtract(int x, int y)
  {
    Console.Write("Subtract({0}, {1})", x, y);
    int result = Decoratee.Subtract(x, y);
    Console.WriteLine(" = {0}", result);
    return result;
  }
}

 

接下来只要在对象工厂的CreateInstance()方法中将其加入对象构造图中即可:

 

public class CalculatorFactory
{
 
 
private static Type[] GetObjectGraph()
  {
    ArrayList objectGraphTypes = new ArrayList();

    // Add decoratee as 1st type to be created

    objectGraphTypes.Add(typeof(Calculator));

    // and then add all decorators
    objectGraphTypes.Add(typeof(CalculatorTimer));
   
objectGraphTypes.Add(typeof(CalculatorLogger));

    return (Type[])objectGraphTypes.ToArray(typeof(Type));

  }

  public static ICalculator CreateInstance() …
}

 

不过,以我有限的设计经验来看,让所有的DECORATOR显式去调用Decoratee的方法以形成对象关系链是繁琐和易出错的(不过也是效率最好的)。因为如果某一个方法实现没有去调用Decoratee的相应方法的话,整个对象链将被切断。同时我们还发现,随着组件接口的日益复杂,接口中方法的数目越来越多,因此对于每一个新增的DECORATOR而言都需要实现所有这些接口方法,这对于一些并非施加于所有接口方法的DECORATOR而言就显得有些累赘了(如仅对少数方法有意义的缓冲策略DECORATOR等等)。这些现象都提示我们可以使用TEMPLATE METHOD设计模式来为所有的DECORATOR提供一个统一的、灵活的基础实现逻辑,如下:

 

public abstract class CalculatorDecorator: ICalculatorDecorator, ICalculator
{
  private ICalculator decoratee;


  void ICalculatorDecorator.InitDecorator(ICalculator decoratee)
  {
    this.decoratee = decoratee;
  }

  public ICalculator Decoratee
  {
    get { return this.decoratee; }
  }

  int ICalculator.Add(int x, int y)
  {
    DoPreAdd(ref x, ref y);
    int result = decoratee.Add(x, y);
    DoPostAdd(x, y, ref result);
    return result;
  }

  int ICalculator.Subtract(int x, int y)
  {
    DoPreSubtract(ref x, ref y);
    int result = decoratee.Subtract(x, y);
    DoPostSubtract(x, y, ref result);
    return result;
  }

  protected virtual void DoPreAdd(ref int x, ref int y) {}
  protected virtual void DoPostAdd(int x, int y, ref result) {}

  protected virtual void DoPreSubtract(ref int x, ref int y) {}
  protected virtual void DePostSubtract(int x, int y, ref result) {}
}

 

在这个基于TEMPLATE METHODS设计模式的DECORATOR基类中,我们显式的实现了ICalculator接口所有方法的模板逻辑,模板中可变的部分委托给若干protected virtual的模板方法实现,这些模板方法继而可被派生类可选择的override(因为是virtual的)。当然,如果你希望所有的DECORATOR一定要提供某一个环节的模板方法实现,你也可以使用protected abstract来修饰这个模板方法,这样一来派生类就必须要实现这个方法才能够通过编译了。另外,虽然我们在基类上使用了显式接口实现,但是派生类仍然可以重新提供相关接口的实现,这种灵活度足以满足各种应用场合了(参见C#语言规范之13.4.113.4.4)。

 

还有一个问题需要解决,即DECORATOR之间的相互协作在目前的设计中是不可以完成的。为什么要DECORATOR之间要协作?举个简单例子,如果我们需要在Logger中顺便记录方法执行的时间,而这个时间是由Timer来测定的话,我们就需要在Logger的实现中取得由Timer记录下来的时长信息(当然,这同时要求Logger位于Timer的外层,这样当Timer返回后Logger才可能获取到Timer记录的信息)。为了达到这个目的,需要为所有组成对象组合的对象实例引入一个公共的信息容器(比如一个使用Hashtable实现的数据字典),不妨称其为Context。我们希望所有的DECORATOR都可以取得这个共享的容器,因此首先我们扩充ICalculatorDecorator接口及其实现:

 

public interface ICalculatorDecorator
{
  void InitDecorator(ICalculator decoratee, IDictionary context);
  ICalculator Decoratee { get; }
  IDictionary Context { get; }
}

 

public abstract class CalcuatorDecorator: ICalculatorDecorator
{
  private ICalculator decoratee;
  private IDictionary context;

  public void InitDecorator(ICalculator decoratee, IDictionary context)
  {
    this.decoratee = decoratee;
    this.context = context;
  }

  public ICalculator Decoratee
  {
    get { return this.decoratee; }
  }

  public IDictionary Context
  {
    get { return this.context; }
  }

}

 

接下来,我们在对象工厂创建对象的时候为复合对象创建并设置context

 

public class CalculatorFactory
{
 
  public static ICalculator CreateInstance()
  {
    Type[] objectGraphTypes = GetObjectGraph();


    ICalculator result = null;
    IDictionary context = new Hashtable();

    foreach (Type calcType in objectGraphTypes)
    {
      ICalculator calcImpl = (ICalculator)Activator.CreateInstance(calcType);

      if (calcImpl is ICalculatorDecorator)
      {
        ((ICalculatorDecorator)calcImpl).InitDecorator(result, context);
      }

      result = calcImpl;
    }

    return result;

  }
}

 

这样,在DECORATOR(也即CalculatorDecorator的派生类)的内部就可以使用Context属性访问整个对象组合中各个DECORATOR的共享存储环境:

 

public class CalculatorTimer: CalculatorDecorator, ICalculator
{
  public int Add(int x, int y)
  {
    DateTime startTime = PreciseTimer.Now;
    int result = Decoratee.Add(x, y);
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    Context["Add.ElapsedTime"] = elapsedTime;
    return result;
  }

  public int Subtract(int x, int y)
  {
    DateTime startTime = PreciseTimer.Now;
    int result = Decoratee.Subtract(x, y);
    TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
    Context["Subtract.ElapsedTime"] = elapsedTime;
    return result;
  }
}

 

public class CalculatorLogger: CalculatorDecorator, ICalculator
{
  public int Add(int x, int y)
  {
    Console.Write("Add({0}, {1})", x, y);
    int result = Decoratee.Add(x, y);
    Console.WriteLine(" = {0} {1}", result, Context["Add.ElapsedTime"]);
    return result;
  }

  public int Subtract(int x, int y)
  {
    Console.Write("Subtract({0}, {1})", x, y);
    int result = Decoratee.Subtract(x, y);
    Console.WriteLine(" = {0} {1}", result, Context["Subtract.ElapsedTime"]);
    return result;
  }
}

 

本例进展至此已经形成了一定的对象结构,让我们小结一下吧。首先,我们提出了一个问题,即在企业系统开发中因为既要满足功能性需求,也需要照顾很多不同方面的非功能性需求,这使得对象方法的实现变得越来越复杂、繁琐、僵硬和难以维护(虽然从执行效率的角度看还是最优化的);接下来我们引入对象结构设计模式DECORATOR将对象必须实现的各方面代码分散到以接口为基础的核心实现对象和若干修饰对象,并进而引入对象创建设计模式将这些对象的构造过程封装起来,通过接口保持与调用代码的松散耦合;最后,我们对已经形成的对象结构进行小的修正与扩充,使其允许各个DECORATOR通过一个共享的Context相互通信。

 

经过这个设计过程,我们最终形成了一个比起最初模型而言更易于扩展的、松散耦合的、较易维护的对象模型,然而我们也面临一个问题:如果我们有很多这样的业务组件都需要同样的一些DECORATOR的话,我们仍然会面临很多重复代码,而且这些代码在面向对象的范畴内是无法消除的。什么意思?以Logger为例,如果给几十个组件的数百个方法都去实现loggingDECORATOR,无疑仍然是个巨大的开发和维护工作。为什么不可以通过面向对象的技术消除?因为我们希望消除的重复是在方法的层次上,而方法已经是对象内部封装的范畴,很难为不同的类型的每个方法提供一个统一的实现(如果要有也应该在所有对象的一个公共基类中——Object?显然进了死胡同……)。那么这个问题该如何解决呢(尤其是在.NET的环境中)?我将在本文的下篇中结合CLR中提供的相关机制探讨在.NET环境下进行AOPAspect-Oriented Programming,面向方面的编程思想)编码的相关技术,包括CLR中的TranspantProxy/RealProxyMarshalByRefObject/ContextBoundObjectContext Attribute/Context Property/IMessageSink等等。

 

如果你有与此话题相关的开发经验或想法,欢迎你写在评论中与我分享。本文难免有不妥之处希望个中高手予以指教,谢谢!:)

posted on 2004-02-02 03:42:00 by jgtm2000  评论(22) 阅读(12210)

Powered by: Joycode.MVC引擎 0.5.2.0