继续文章的第一部分,我们在这一篇文字中将从另一个角度考虑原文中提出的问题,并深入探索.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 传递给对象外部的时候,这个引用是直接指向真实对象的引用!所以你精心设置的关键的比如说访问控制器等机制就将被绕过了!怎么样才能保证外部调用总是经过我设置的重重关卡呢?
-
更多的不爽请你通过反馈发送给我 :)
其实这些内容本来也是计划在本文中一并完成的,可是篇幅所限(借口!)我想还是留到下一篇文字中继续分析并解决吧!其间还是一如既往的请你把与本文相关的意见、建议、收获、疑惑等等通过留言或者邮件告诉我,您们的支持是我写作的最大动力!再次谢谢大家的阅读!:)
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/24/2004 3:32 PM by progame“因为层次的定义完全在数据库中进行,所以非常灵活。”
这个层次的定义是不是指的“o/r mapping”
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/24/2004 7:52 PM by JGTM'2004 [MVP]关于这个问题我确实是没有在文章中找到合适的机会进行解释。实际上,为什么我一直说这是.NET Remoting Infrastructure呢,因为.NET Remoting架构的runtime部分其实就是在这个基础结构之上的一个实现而已(这个实现是一个未公开的internal类型,名称叫RemotingProxy,也就是一个从RealProxy派生而来的真实代理);除了支持runtime行为的部分,.NET Remoting还有负责远程对象配置管理的一大块内容。
因此说,真实代理机制本身只是完成两个世界转换的任务:从堆栈世界进入消息世界、再从消息世界回到堆栈世界。而在消息世界里面到底干了什么都是你自定义的代理说了算的——包括远程访问的位置信息。
换言之,既然我在文中写的真实代理并没有处理对象位置透明性的企图,它们对对象位置当然是既不知情也不作考虑的——其实跟.NET Remoting毫无关系(我们只是借助了它的地基实现了方法拦截而已)!
不过:如果你将一个.NET Remoting远程对象(也就是说:一个以RemotingProxy为真实代理的透明代理对象)作为目标传入我文中描述的RealProxy作为target,那么方法拦截确实可以在该对象上起作用(这个机制实际上是在RemotingServices.ExecuteMessage()中加以实现的,即:如果target是一个透明代理,则将需要执行的方法调用信息转发给透明代理对应的真实代理的Invoke()方法继续执行)。
关于这方面的问题,我打算稍后再写些文字,说说AOP与.NET Remoting之间的整合话题,不知是否有朋友感兴趣呢。:)
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/24/2004 8:03 PM by JGTM'2004 [MVP]嗯,这可能给您的技术背景有关系,也可能跟我文章的定位有关系。如果关于第一篇利用OOP和设计模式解决分离对象职责方面有任何问题,可以与我联系。:)
@Dreamaster: 但跟AOP好像没有关系啊?
AOP最基本的一个机制就是通过注入代码机制(无论是编译期的还是运行时的)将跨对象类型的aspect代码逻辑分离出来,实现更高内聚的对象。本文即是讲述在.NET中利用其Remoting Infrastructure来实现运行时方法拦截与代码注入的。不过确实没有花篇幅好好解释AOP,因为我也只是了解各皮毛而已,不好班门弄斧啦!:)
@Redmoon: 看来,要边看,边写,才能真正的理解。
确实如此,我自己写过文章以后转换为读者的身份自己跟着文章走了一遍,花了大约两小时。如果你大概浏览一遍文章后觉得这里面的知识是你所需要的,那么建议你也花上两个小时跟着学一下,这样我也就平衡了呀!:)当然,如果现在没什么用的话,收藏一下就好了,嘿嘿!
@john_masen:
还没有太理解你的意思,感觉是一个完全通过配置数据来定义的运行时?有点儿像解释执行的BASIC?这样,因为我的MSN平时经常连接不上,请你通过左面的“联系”功能发邮件给我,我们邮件探讨如何?:)
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/25/2004 8:58 AM by kain对Imessage等接口的使用描述。对于这样
的文章我比较喜欢Csdn开发高手中大话Design
的那种方式。
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/25/2004 10:44 AM by JGTM'2004 [MVP]被你搞晕了——到底是觉得第一篇不好懂还是第二篇不好懂?不过实事求是的,我建议初学者还是先把基础知识掌握好,本文中提到的机制并不是适用于所有场合,它只是给你多一种解决复杂问题的机制——尤其当别的办法都不太好用的时候(所以文章后面我也写到了如何结合具体问题选择具体机制的话题)。真正理解了这些机制的内部机理之后,在实际开发中还是以参考MSDN Library文档为主吧!:)
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/25/2004 12:14 PM by microhelper拼写错误
Calculator realCalculator = new Caculator();
Caculator应该是Calculator
>>经过这样的改造,当我们再遇到Invoke()调用时,就可以访问到最开始传入的真正干活的Calculator对象了
此时的状态是
....
public override IMessage Invoke(IMessage msg)
{
IMethodCallMessage callMsg = msg as IMethodCallMessage;
int x = (int)callMsg.InArgs[0];
int y = (int)callMsg.InArgs[1];
Calculator calc = Calculator.CreateInstance();
int result = calc.Add(x, y);
return new ReturnMessage(result, null, 0, null, callMsg);
//
// IMethodCallMessage callMsg = msg as IMethodCallMessage;
// IMessage returnMsg = RemotingServices.ExecuteMessage(target, callMsg);
// return returnMsg;
}
....
....
public static Calculator CreateInstance()
{
Calculator realCalculator = new Calculator();
RealProxy realProxy = new MyRealProxy(realCalculator);
object transparentProxy = realProxy.GetTransparentProxy();
return (Calculator)transparentProxy;
}
....
此时运行测试是失败的,因为Invoke里面调用int result = calc.Add(x, y);实际上还是通过Invoke进行的,所以会发生死循环,用
RemotingServices.ExecuteMessage才不会出问题
>>回过头来仔细观察上面的代码,可以发现真正对核心真实对象(target)方法的执行就是发生在调用RemotingServices的ExecuteMessage()
方法之时。在它之前,我们可以通过callMsg取得(甚至修改)所有的关于方法调用的信息(就是AOP基本操作之pre-processing啦);在它之
后,我们又可以通过returnMsg取得(甚至修改)所有关于方法返回的信息(也就是AOP基本操作之post-processing啦!
>>[2004-02-16 12:34:56] Calculator.Add(3, 5) -(37ns)-> 8
遍历参数判断要访问IMessage msg,如果Calculator.Add的接口改变,比如添加一个参数,那么记录日志的代码是不是就不工作了?
实际运行时,要跟据定义好的商业逻辑做不同的动作,商业逻辑可能非常繁杂,比如你向我借钱,借一块钱,我假装爽快地给你了,借10块钱
,我要找个账本记下来,借100块,我偷偷察看了一下,你10年前借了一毛钱还没还,超过100块,俺还向mm请示,借钱之后根据还要钱数不同
套用不同的催债策略。。这些逻辑可能经常变化
pre-processing/ post-processing 职责链方式,是不是还不太方便?
如果我有些组件只想按照普通的方式用Calculator呢?
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/25/2004 2:16 PM by kain其实我的意思是我想通过你的文章来学习
如何运用AOP的方法来发现、解决实际的问题。
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/25/2004 10:57 PM by kaneboy感想:
1、记得后面有一个地方说这是用复杂方法说明简单问题,表示反对,这篇文章写得深入浅出,原理很清楚。
2、MessageSinkBase的SyncProcessMessage()的返回值应该是IMessage类型。
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/25/2004 11:07 PM by flier虽然这个RealProxy号称透明代理,但实际上就如你所说,其透明程度还仅仅局限于普通方法调用的单向透明。因此要实现构造函数的接管,以及在对象方法内调用本对象方法的接管,需要使用其他的特定模式。而这样一方面增加了薄记工作,另一方面限制了其适用的范围(例如不适用于别人提供的类型)。
而简洁性是另外一个很重要的方面。大概也可能是因为文章只是给出一个例子,并没有考虑太多使用上的简洁性和灵活性。个人认为基于属性和外部XML配置文件的动态调整机制是不可少的,否则灵活性大大降低。而这就需要对目前这种基于硬编码的实现做相当的调整。
最后还有效率上的损失。我看了一下rotor中的相关实现,构造透明代理的代码效率还是很高的,直接在vm中通过操作vtbl完成;但调用方法的代价未免太大了,先要封装到一个IMethodCallMessage,进入后还得通过StackBuildSink构造调用堆栈,完了还得把结果封了又拆。如果对于事务或者安全这种复杂的切入尚可接受,对普通的诸如写日志等轻量级操作来说,代价未免太大了。
总而言之,个人认为这种基于remoting的实现,模拟尚可。真要实际大规模使用,还是得老老实实发展.NET AOP的Native实现。毕竟.NET提供了reflection.emit等强大工具,起点就比java高。
我大概反编译看了一下LOOM.NET的实现,他实际上就是用Emit动态构造了一个类似透明代理的封装,这样可以解决简洁性和效率方面的一些问题。但如果真正要完全解决,我还是比较倾向于使用Method Proxy的实现思路,虽然用unsafe code做有些dirty。
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/25/2004 11:35 PM by kaneboy3、个人感觉,用现成的IMessageSink接口不是很合适,因为可能在概念上造成模糊。我觉得用一个自己定义的类似的比如“IAOPHandler”可能更好:
public interface IAOPHandler
{
IAOPHandler NextHandler { get; }
IMessage HandleInvoking(IMethodCallMessage msg);
}
功能一样,但我感觉这个名字能更反应其特征。
相应的,另外的类也改名为比如:TerminatorHandler,ChainHandlerProxy...
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/26/2004 12:24 AM by john_masen<SOAAttribute(gettype(SOATraceService),gettype(SOAPerformenceTraceService)>public sub foo()
'do something
end sub [推荐]
<SOATraceService,SOAPerformenceTraceService>public sub foo()
'do something
end sub [不推荐]
更进一步来说,我发现通过SOA模板似乎更好一些,比如这样的代码
<SOATemplate(TemplateName:="Default")>public sub foo()
'do something
end sub
使用模板的好处是代码简单,而且模板是可以放在配置文件中,维护简便,修改容易。并且在模板中可以针对不同的方法来返回不同的SOAService配置数据,比如对象构造函数就最好使用一个单独的配置数据节。
还有一个问题,因为请求的SOA服务都已经在属性中注明,如果在实际使用中需要替换的话需要重写SOAServiceFactory或者重新编译代码(这个好像不太现实)。 为了避免实现强类型的服务请求,可以通过请求SOA接口来实现。SOAServiceFactory通过检索已经注册的接口实现来返回实例。这样就可以很容易的替换SOA服务类,就算新的东西由别的公司开发没有实现你的SOA接口,通过Adapter模式也是很容易的。SOAService可以注册为一个windows服务,所有的客户端可以不必去写自己的配置文件,这在由很多子系统分别开发的工程中很有用。举例:
定义:
public class SOAAttribute
public sub(ParamArray ServiceInterfaces() as type)
......
end sub
end class
-------------------------------------
使用:
<SOAAttribute(gettype(iSOATraceService),gettype(iSOAPerformenceTraceService)>public sub foo()
'do something
end sub
当然,上述的方法也可以和SOATemplate结合起来使用。
关于SOA如何使用,因为在企业的业务流程中,通常一个业务操作会对应一个的对象调用链,那么一般来说没有必要为每一个对象调用启用相同的SOA功能,这既增加了代码量,而且使用也很不方便。想象一下,你写的每一个方法前面都要加上一大堆相同的SOA属性,岂不是很麻烦。为了简化操作,我们可以通过在根对象上面设置SOA模板,而Proxy把SOAService实例放在对象实例的上下文中(从System.ContextBoundObject派生)来实现。对象可以通过重写<SOAAttribute>来避免继承SOA服务属性,或者不定义SOAAttribute来默认继承SOA服务属性。比较麻烦的是System.ContextBoundObject被微软认为是内部对象,不提供文档,要摸索需要一点时间。当然,也可以使用数据槽,虽然这样比较不太符合我这种OO Freak的习惯 :) 。还有一种办法是在ObjectFactory中加入上下文对象支持,在初始化时注入SOA上下文,不过这要求对象都从指定的对象派生(比如SOAFramework.ContextBoundObject),呵呵。
暂时就想到这里,歇一会,以后再讨论 :)
ps: [to楼主:mail我已经发出了。]
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/26/2004 12:25 AM by john_masen# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/26/2004 12:28 AM by john_masenSOATraceService和SOAPerformenceTraceService并不是基类和派生类的关系。
应该写成SOALoggerService和SOAPerformenceTraceService好一些
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/26/2004 11:27 AM by JGTM'2004 [MVP]真是火眼金睛!看来我应该早先声明,所有代码都是示意性的,不保证可以拷贝粘贴,还是理解文字中的内容以后自己独立编写的为好——你看,我写文章时候的代码就不是从VS.NET里面拷贝过来的噢!嘿嘿 ;-)
当文字要引出RemotingServices.ExecuteMessage()方法的时候,确实存在你所说的那个不可测试通过的中间状态,我想应该是我遣词造句之不妥,我已改成一个设问句,提示读者本步改造尚未完成。这里真要谢谢你的认真实践,而我相信你一定也从本文学到了更多的东西!:)
>> 如果Calculator.Add()的接口改变,比如添加一个参数,那么记录日志的代码是不是就不工作了?
我估计你是越看越没耐心啦,也怪我的文章太长了(应该按照45分钟的标准切割——一堂课嘛)。首先,记录日志的代码是留给读者自己写的,如果你的代码依赖于参数数目或者,那么当然会出现你提出的问题。不过要我写,我还是会去枚举IMethodMessage.InArgs数组的,这样不就不会出现你说的问题了吗?如下:
ArrayList argStringList = new ArrayList();
foreach (object arg in callMsg.InArgs)
{
argStringList.Append(arg.ToString());
}
string[] argStrings = (string[])argStringList.ToArray(typeof(string));
string argListString = String.Join(", ", argStrings);
注意,这段代码并没有进行性能优化,但是很容易理解。其实仔细想想,一旦进入消息的世界,方法的signature变得不很重要,所以针对方法消息的处理代码也就变得对方法本身的含义不甚敏感。而这正是AOP范畴要解决的问题。我们就是要把那些对对象内部封装的方法不敏感的非功能性代码转移到对象的外部,甚至能够重用——最容易理解的就是方法日志这样的代码了。
因此,后面你提到的问题也就不是问题了,因为你的商业逻辑部分代码根本不是本方法所能够提出并重用的部分!可是你又想把这部分代码从原有的方法代码中独立出来,这个问题怎么解决?那么请你复习本文的第一部分,通过实现*相同的方法接口*并利用对象工厂组合对象,你的每一部分代码都可以在方法的语义范畴内进行针对性的处理——非针对性处理还是本文的方法比较合适吧(“行为至此”那段不是很清楚的说到了这个问题吗?:)。
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/26/2004 12:03 PM by JGTM'2004 [MVP]谢谢你的反馈!又帮我发现一处低级错误……惭愧啊!:'(
另外,你关于IAOPHandler的想法完全正确!事实上我也为这件事情反复斟酌了多次,不过最后还是直接采用了.NET内部的IMessageSink接口。原因有几点:首先,这个接口在.NET Remoting中是非常非常重要的一个基本接口,熟悉.NET Remoting的朋友应该对他都不陌生(而且就算你现在不知道它,以后深入开发时也会或多或少的与之打交道);第二,相比IAOPHandler,它只是多了一个对异步方法消息处理的支持,应该不会对读者理解其本质产生太大的影响;第三,我下一篇文章中讲到context property的时候还要用到这个接口提供的机制,所以这里也算埋下一个伏笔,也使得现在写的IMessageSink以后可以直接重用在另一个环境中,希望老兄能够理解偶的用心。:-)
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/26/2004 12:57 PM by JGTM'2004 [MVP]谢谢你的反馈!你提到的三个问题确实是本文在讲述解决问题的方法之外也希望引发读者思考的几个问题(不过在下一篇文字中我将继续介绍.NET中可以解决前两个问题的机制)。这里我先对这几个问题详细解释一二。
其实,RP/TP机制是可以接管构造器方法调用的(也就是在IMessageCallMessage/IMessageReturnMessage的基础上还有IConstructionCallMessage/IConstructionReturnMessage),且这一块基于IActivator的机制还是很巧妙、很灵活的!但是从本文的方法来看,只有接管了new指令,才有可能拦截到构造器方法调用,而如果对所有对象都采用这个机制,势必对本地对象性能产生很大的整体性影响,因此.NET/CLR只对基于ContextBoundObject的类型提供这种支持,而这个正是下一篇文字中要探讨的核心话题。至于说要对别人提供的类型也进行方法拦截的处理,这里面可能涉及更多的问题了:安全性、效率等等……所以CLR只是在COM的层次提供了一个Profiler API,不过目的显然不是解决AOP要解决的问题了。至于说对象方法内部调用,目前的默认机制都是不支持的(一方面是效率原因,一方面也大概是这种设计的必然结果),不过我正在研究这个问题,看看有没有比较理想的解决办法(RealProxy的构造器提供了一个IntPtr类型的stub,这可能会是问题的突破口)。
至于简洁性,本来是要写到文章中的,但是想了想还是觉得先把基本原理讲清楚,再在下一篇文章中给出全貌的好。事实上,在我们项目的实际应用中确实大量的结合了自定义属性和外部配置文件的机制提供部署阶段的灵活性(比如说可以在系统运行期动态开关性能检测sink、动态切换调试模式、逐组件屏蔽权限管理decorator……等等)。我想,有了文中介绍的基本机制,这些灵活性扩展应该自然不在话下了吧(自定义属性可以通过IMethodMessage.MethodBase取得;而对XML配置文件的支持更是简单到只要引用System.IO/System.Xml进行开发就可以了,跟基础机制就没什么关系了)。
关于效率方面的问题,这跟文中所提机制的设计初衷很有关系!为什么我一直说它是.NET Remoting Infrastructure而不是.NET Infrastructure呢?就是因为它本来就是为构建.NET Remoting层而设计的基础机制,而并没有考虑到提供所谓的AOP开发(虽然Remoting本身也就是一个AOP:对象位置透明性)——而Remoting本身的性能开销使得RP/TP的开销成了可忽略的(当然对于本地对象还是必须要考虑的了,我想这也就是为什么微软一直也没有官方提到利用该机制支持AOP开发的原因吧)。我浏览了网上很多的文章,发现了一些线索。比如说,刚了解这个机制之后我就想,COM Interop应该也是构建于这个基本架构上的吧(利用透明代理将基于接口的调用通过P/Invoke转发到COM对象上)!其实不然!Microsoft CLR一个大牛也说到,因为CLR关于COM Interop的开发在时间线上是先于.NET Remoting子系统的,所以当时并没有采用RP/TP的机制——而现在看来,如果能够合并到这个机制上才够完美,而且更关键的是,如果COM Interop就是构架在RP/TP机制上的话,Microsoft也势必要对这个层次的性能问题进行更大的投入(已有证据表明存在相当多的可优化点对本地对象提供shortcut而大大提升性能),使其达到可以应用于非Remoting的本地场合,那样的话这个死穴就不存在了——可是,历史就是历史,我们只能展望未来了:据说在未来的CLR实现中这个层次对本地对象的性能优化将有*数量级*的改善!让我们拭目以待吧。
所以说,老兄的呼唤其实也正是我们所有在.NET平台上进行企业级应用开发的软件开发、设计师的呼唤,希望.NET能够成为真正适用于大型、高效、企业级应用开发的完美平台——而对AOP机制native的支持只是其中重要的一环而已。我们也欣喜的看到在2004年内将有Whidbey/Whitehorse/MBF等等对.NET平台的重大补充,这真将是一个值得期待的猴年马月啊!:)
BTW: 你的blog写得很棒,我已经收订了!:-)
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/26/2004 1:03 PM by JGTM'2004 [MVP]思路没错!如沐春风啊!欢迎您就相关问题继续发表评论!:)
PS: 邮件收到,稍后与您详细探讨!
# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
2/28/2004 1:13 AM by john_masen哎,TMD项目实在太急,四月一号要上线居然现在才定下需求。国企里面做程序员简直不是人干的!
# Unit Testing with .NET Quiz: Adaptable Mock Objects?
3/2/2004 9:10 PM by JGTM'2004 .NET Blog# 回复: A Taste of AOP from Solving Problems with OOP and Design Patterns (Part II)
3/29/2004 9:02 PM by JGTM'2004 [MVP]chainedProxy.AppendSinkType(typeof(MyMessageSink1));
chainedProxy.AppendSinkType(typeof(MyMessageSink2));
上面代码中的MyMessageSink1和MyMessageSink2原文中没有给出实现方法,我自己想了半天想不来,可否给我发一个例子过来,好让我明白这步是怎么实现的,谢谢!
答复:
注意到在AppendSinkType方法中我们是利用.NET的Activator来动态构造给定的对象实例,同时传入了代表nextSink的一个构造参数。这就是说,我们在AppendSinkType方法中给出的类型应该有一个可以接受IMessageSink作为NextSink的构造器。再注意到我们把构造成功的实例转换为MessageSinkBase,所以只需要从这个类派生你的消息处理器就可以了。比如说,最简单的什么都不做的:
public class MyMessageSink: MessageSinkBase
{
public MyMessageSink(IMessageSink nextSink): base(nextSink) {}
public override IMessage SyncProcessMessage(IMessage msg)
{
return NextSink.SyncProcessMessage(msg);
}
}
以此为基础,可以非常简单的扩展你自己的消息处理逻辑。