1. 程式人生 > >關於用Delphi實現動態代理

關於用Delphi實現動態代理

本來想上週末沒能用DELPHI實現動態代理就算了,可是這幾天卻始終放不下這個想法,這實在是一個太美妙的想法了。而且在認真看了VCL對SOAP的實現後,現在至少有九成的把握可以實現這樣一個動態代理。

那麼動態代理有什麼用?

這要先從GoF的Proxy模式說起。

假設有下面這樣一個介面及其實現:

o_foo1.png

現在,如果你是這個介面的使用者,而這個介面及其實現者提供了一個:

  Foo : IFoo;

給你,其中Foo指向TFooImpl的一個例項。現在你有了IFoo的定義,和這個Foo例項--注意,你沒有TFooImpl的定義和實現程式碼。如果現在要求你為所有的IFoo.doSth增加事務功能(假設doSth被實現為對資料庫作更新操作),要怎麼辦?

GoF的Proxy模式就是解決方案之一:

o_foo2.png

如果所示,首先要實現一個新的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 TStaticProxy.Create( aImpl : IFoo );
Begin
  FImpl := aImpl;
End;

function TStaticProxy.doSth( ... ) : xxx;
Begin
  BeginTransaction;
  Result := FImpl.doSth( ... );
  EndTransaction;
End;

function TStaticProxy.bar( ... ) : xxx;
Begin
  Result := FImpl.bar( ... );
End;

然後,在所有需要用到Foo物件的地方,改用新的NewFoo物件,如下:

var
  NewFoo : IFoo;
Begin
  NewFoo := TStaticProxy.Create( Foo ) As IFoo;
  ...  //  之後就可以把NewFoo完全當作Foo一樣使用了。
End;

可見,我們通過了一個Proxy類代理了所有對IFoo介面的操作,相當於在從IFoo到TFooImpl之間插入了自己的程式碼,在某種程度上,這就是AOP所謂的“橫切”。當然如果你能有TFooImpl的程式碼的話,就簡單了,只要如下:

o_foo4.png

從TFooImpl派生一個TNewFooImpl,然後在其中Override一下TFooImpl中的doSth即可,然後就建立TNewFooImpl的例項來代替Foo引用即可。

但問題就在於你必須擁用TFooImpl的程式碼,並且可以變更所提供的Foo例項,但這在很多時候是做不到的--除非不是用DELPHI,而是如 Python一類的動態語言^O^。比如元件容器,比如遠端例項等。還有像“虛代理”(就是當建立FImpl代價很高時,就在建立時只建立代理類,然後在真正需要時才建立FImpl的例項)

但上面這種靜態代理還是很麻煩。首先,如果IFoo的成員函式很多的話,必須要一一為它們加上代理實現;其次,如果在應用中有很多介面需要代理時,就必須一一為它們寫這樣的專用代理類;第三,需要變更代理功能時,需要修改所有的代理類……

特別是像元件容器或是通用遠端代理這樣,對要實現的介面並不能確定的情況下,靜態代理一點用也沒有。

所以我們需要“動態代理”。我是在看了GIGIX發表在今年第一期《程式設計師》上的《動態代理的前世今生》一文後,雖然他說是的JAVA在 JDK1.3中提出的,在java.lang.reflect中的proxy。但這卻讓我突發奇想,發現其實完全可以在DELPHI裡也實現這樣一個動態代理。

一個典型的動態代理如下:

o_foo3.png

這樣,我們只需要把要增加在功能做成一個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 TFooInvHandler.Create( aImpl : IFoo );
Begin
  FImpl := aImpl;
End;

function TFooInvHandler.Invoke( 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 := TFooInvHandler.Create( Foo );
  NewFoo := TDynamicProxy.Create( 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的內部複雜性,易用性太差。

這就是我現在還不能確定實現的那一成。-_-|||

猛禽 Feb.03-05