A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II) zz
Posted on 2008-04-06 18:09 [虫子] 阅读(345) 评论(1) 编辑 收藏 举报继续文章的第一部分,我们在这一篇文字中将从另一个角度考虑原文中提出的问题,并深入探索.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#面向对象编程的知识而言,只有当LoggerProxy是Calculator的派生类的时候,上面的类型转换代码才可能在运行期成立。而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周游天下的话,它或者要是MBV(marshal by value),或者要是MBR(marshal 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(更具体的说——其中提供的数据字典);而那个“能够处理它的代码”自然就是我们真实代理对象内部的代码咯!也就是说,透明代理帮我们截获了来自调用代码基于堆栈的所有方法调用,并将其打包成数据字典以方法调用消息的形式转发给我们的真实代理进行在高级语言层次上的处理——这就是本篇文字要讲述的核心问题,即利用CLR的TP/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.Key和entry.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)方法的执行就是发生在调用RemotingServices的ExecuteMessage()方法之时。在它之前,我们可以通过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的透明代理首先将堆栈上的信息打包传给LoggingProxy的Invoke()方法;然后你在LoggingProxy的Invoke()内调用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接口中的两个方法:NextSink的getter因为觉得不太可能需要灵活改写,所以实现为默认的非虚方法了(嗯?怎么成方法了,不是属性吗?别忘了属性其实就是一对或一个方法噢!);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
{
…
}
…
}
现在我们就可以在Calculator的FACTORY 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的透明代理截获并转换为方法调用消息,随后被发送给对应的真实代理也就是我们的MessageChainProxy的Invoke()方法处理,而该方法立即把该消息传递给headSink也就是最后被添加到处理链上的访问控制处理器的SyncProcessMessage()方法上。此时,如果访问控制器允许方法被继续执行下去,则同样的方法调用消息被转发给NextSink所指向的方法日志处理器处理,最终方法消息到达TerminatorSink并被转发给RemotingServices的ExecuteMessage()方法——还记得前面讲过的这个方法的执行逻辑吧:一个透明代理的“反透明代理”也就是StackBuilderSink(你看,它的名称已经暗示了它其实也是一个IMessageSink!)最终利用传入的方法调用消息重建调用堆栈,并发往真正干活的我们的Calculator类……但是如果在访问控制器这一关没有过去,而是被抛出了安全异常的话(要记得用ReturnMessage构造一个代表方法调用异常的返回消息噢!不然的话透明代理也就不再透明了……),显然方法日志这一环节根本就不会被执行到了(更甭说随后的TerminatorSink和StackBuilderSink了,当然我们对权限控制完全无知的Calculator也就永远不会看到这个越权的方法调用了)。所以说,如果你希望记录下所有的方法调用请求,就应该把方法日志处理器放到权限控制的前面来,也就是说——在添加完访问控制器以后再添加日志处理器就行了。这个道理还是比较简单的,和前面说的DECORATOR模式的实现是一样的——我这里再捋一遍也就当作本篇文字的再回首吧。
行文至此(好拽),我们已经对基于TP/RP和IMessage的方法拦截机制有了初步的了解,利用这些知识应该已经可以解决很多问题了。那么前文中描写的基于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传递给对象外部的时候,这个引用是直接指向真实对象的引用!所以你精心设置的关键的比如说访问控制器等机制就将被绕过了!怎么样才能保证外部调用总是经过我设置的重重关卡呢?
-
更多的不爽请你通过反馈发送给我 :)