本来想上周末没能用DELPHI实现动态代理就算了
可是这几天却始终放不下这个想法
这实在是一个太美妙的想法了
而且在认真看了VCL对SOAP的实现后
现在至少有九成的把握可以实现这样一个动态代理
那么动态代理有什么用?
这要先从GoF的Proxy模式说起
假设有下面这样一个接口及其实现
现在如果你是这个接口的用户而这个接口及其实现者提供了一个
Foo : IFoo;
给你其中Foo指向TFooImpl的一个实例现在你有了IFoo的定义和这个Foo实例注意你没有TFooImpl的定义和实现代码如果现在要求你为所有的IFoodoSth增加事务功能(假设doSth被实现为对数据库作更新操作)要怎么办?
GoF的Proxy模式就是解决方案之一
如果所示首先要实现一个新的IFoo接口实现TStaticProxy其中用了一个属性FImpl记录了TFooImpl的实例然后在 TStaticProxy中实现doSth和bar并且将不需要变更的bar函数直接委托给FImpl处理而在doSth的实现里加入事务处理即可 TStaticProxy的代码大致如下
TStaticProxy = class( TInterfacedObject IIFoo )
private
FImpl : IFoo;
public
constructor Create( aImpl : IFoo );
function doSth( ) : xxx;
function bar( ) : xxx;
end;
constructor TStaticProxyCreate( aImpl : IFoo );
Begin
FImpl := aImpl;
End;
function TStaticProxydoSth( ) : xxx;
Begin
BeginTransaction;
Result := FImpldoSth( );
EndTransaction;
End;
function TStaticProxybar( ) : xxx;
Begin
Result := FImplbar( );
End;
然后在所有需要用到Foo对象的地方改用新的NewFoo对象如下
var
NewFoo : IFoo;
Begin
NewFoo := TStaticProxyCreate( Foo ) As IFoo;
//之后就可以把NewFoo完全当作Foo一样使用了
End;
可见我们通过了一个Proxy类代理了所有对IFoo接口的操作相当于在从IFoo到TFooImpl之间插入了自己的代码在某种程度上这就是AOP所谓的横切当然如果你能有TFooImpl的代码的话就简单了只要如下
从TFooImpl派生一个TNewFooImpl然后在其中Override一下TFooImpl中的doSth即可然后就创建TNewFooImpl的实例来代替Foo引用即可
但问题就在于你必须拥用TFooImpl的代码并且可以变更所提供的Foo实例但这在很多时候是做不到的除非不是用DELPHI而是如 Python一类的动态语言^O^比如组件容器比如远程实例等还有像虚代理(就是当创建FImpl代价很高时就在创建时只创建代理类然后在真正需要时才创建FImpl的实例)
但上面这种静态代理还是很麻烦首先如果IFoo的成员函数很多的话必须要一一为它们加上代理实现其次如果在应用中有很多接口需要代理时就必须一一为它们写这样的专用代理类第三需要变更代理功能时需要修改所有的代理类……
特别是像组件容器或是通用远程代理这样对要实现的接口并不能确定的情况下静态代理一点用也没有
所以我们需要动态代理我是在看了GIGIX发表在今年第一期《程序员》上的《动态代理的前世今生》一文后虽然他说是的JAVA在 JDK中提出的在javalangreflect中的proxy但这却让我突发奇想发现其实完全可以在DELPHI里也实现这样一个动态代理
一个典型的动态代理如下
这样我们只需要把要增加在功能做成一个IInvocationHandler接口的实例如图中的TFooInvHandler然后动态创建一个支持IFoo接口的TDynamicProxy的实例它是一个动态代理只需要传入相应的参数要实现的接口和相应的InvHandler实例即可不需要为每个接口写一个代理当然如GIGIX文中所说对于C++来说这个可以用模板实现但问题在于模板归根到底是一种编译时的动态化技术对于组件容器这样需要运行时动态化的应用它还是不能实现最后InvHandler通过RTTI去调用具体的实现Foo
它的用法大致如下
TFooInvHandler = class( TInterfacedObject IInvocationHandler )
private
FImpl : IFoo;
public
constructor Create( aImpl : IFoo );
function Invoke( IID MethodName Args[] ) : xxx;
end;
constructor TFooInvHandlerCreate( aImpl : IFoo );
Begin
FImpl := aImpl;
End;
function TFooInvHandlerInvoke( IID MethodName Args[] ) : xxx
Begin
If ( IID = IFoo ) AND ( MethodName = doSth ) Then
Begin
BeginTransaction;
Result := DoInvoke( FImpl IID MethodName Args[] );
EndTransaction;
End
Else
Result := DoInvoke( FImpl IID MethodName Args[] );
End;
var
Handler : IInvocationHandler;
NewFoo : IFoo;
Begin
Handler := TFooInvHandlerCreate( Foo );
NewFoo := TDynamicProxyCreate( TypeInfo(IFoo) Handler ) As IFoo;
//之后就可以把NewFoo完全当作Foo一样使用了
End;
注意其中IInvocationHandler接口我还没想好要怎么定义所以这段代码只是大致说明一下问题另外其中的DoInvoke就是通过RTTI来调用FImpl的
从上面的代码可以看到TDynamicProxy通过参数IFoo动态生成了一个对IFoo接口的代理并且通过Handler参数插入一个处理接口IInvocationHandler由TDynamicProxy把对IFoo接口的调用全部转成对IInvocationHandler接口的调用最后由TFooInvHandler类来视情况处理在这里可以通过运行时配置方式来动态决定是否需要切入事务所处理需要对哪个接口的哪个方法切入
有了这样一个动态代理还可以很方便地在InvocationHandler里切入如安全性检查LOG等这样的话用DELPHI来实现AOP也不成问题了
现在我面临的问题就是如何来定义这个IInvocationHandler
其实这里最主要的问题就是参数的传递的问题接口可以用IID表示方法可以用方法名但参数变化太多了一是数量不确定可以有任意多个参数二是类型不确定三是传值参数和引用参数的问题如前面那个例子用的是简单的办法就是用一个不定长的Variant数组来记录可以解决前两个问题但第三个问题就比较麻烦难道要用一个Tuple来作返回值?太麻烦了吧
在VCL的SOAP实现里是通过一个TInvContext在记录的但这样的话对于Handler的开发者来说就不得不面对TInvContext的内部复杂性易用性太差