现在的位置: 首页 > 综合 > 正文

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

2011年03月13日 ⁄ 综合 ⁄ 共 11634字 ⁄ 字号 评论关闭

继续文章的第一部分,我们在这一篇文字中将从另一个角度考虑原文中提出的问题,并深入探索.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

抱歉!评论已关闭.