非COM环境下的接口编程
------问题,技巧,应用
先行知识:Delphi/接口/Dll/OOP
难度:★★★☆☆
引子:
接口的概念由来已久,早在COM出现之前(COM应该是95年左右)接口的概念就已经在面向对象的开发中根深蒂固了,著名的《设计模式》(94年出版)中也指出“针对接口编程而不是针对实现编程”。使用接口可以降低软件系统中不同模块的偶合性,利于软件系统的更新与维护。接口的优点绝对不只是出现在COM中,事实上在大多数的编程任务中接口都是一个不错的选择。(用delphi开发过Web Service的朋友知道,delphi也是使用接口来描述Web Methord的,所以接口的概念在面向对象领域永远不会过时)本文不是一篇讨论COM的文章,而是想通过一个例子来说明在delphi中接口的实际作用,以及在开发中可能碰到的问题和所需的技巧。
例子:
※第一印象:
熟悉windows程序设计的人应该早已经在他们开发的系统中使用到了DLL,如果我们要把对象放入DLL中维护(而不仅仅是一些函数和过程)怎么办呢?最容易想到答案是使用COM。除此之外还有什么办法呢?使用delphi中的动态包bpl或则一些其他的一些办法(如内存拷贝)也许可以解决问题。不过现在我们要创建一个标准的DLL文件,我们可以象使用COM一样直接通过接口来操作维护在其中的对象,但又不用象COM组件一样需要注册,它应该是如同普通的DLL文件样只要加载就可以正常工作。这样的优点是明显的,也许我们正在需要一个如同大多数绘图软件一样允许有插件扩充的程序,那么除了标准的COM技术外我们可以将实现约定接口(也就是插件的契约)的对象放在一个标准的DLL库中,在主应用程序中根据一份可由用户配置的文件中的不同插件名称和所在路径来依次加载这些DLL,这样我们的插件下载到客户的计算机中后根本不用任何注册安装过程,而仅仅只是在主程序中配置它就可以正常工作了。这个过程看起来象这样:
for I:=0 to PluginCount-1 do
//PluginCount是从配置文件中得到的已经“安装”的插件数目
begin
…
Dllhnd[i]:=loadlibrary(PlugPath);
//PlugPath为每一个dll的路径,以由前面程序从培植文件中得到
@GetPlugIntf:=GetProcAddress(Dllhnd[i],’GetPlugIntf’);
PlugIntf[i]:= GetPlugIntf; //GetPlugInth可以返回一个IunKnown的接口
…
end;
现在我们就得到所加载的每一个插件的接口并可进行操作了。从上面的代码中可以大概的看出一些我们需要管理对象的DLL的样子:这个DLL只有一个唯一的导出函数以获得其中维护的对象的接口(GetPlugIntf,也有可能有其它的导出函数,但这个是必须的),这个函数可以返回一个对象实现的接口也可以直接返回Iunknown接口(这样便于用一个数组管理所有的插件接口,也利于用循环结构实现程序,就象上面看到的那样),主程序在需要的时候进行转换。另外我们的主程序需要和Dll共用一个描述接口的文件(契约)。返回接口导出的函数看起来象这样:
var
OurObject:TintfObject;
…
function GetFooObjectIntf:IUnKnown;stdcall;
begin
if not assigned(OurObject) then
begin
OurObject:= TintfObject.Create;
…
end;
result:= OurObject as IUnKnown;
end;
有了上面的描述后可以看到要在一个普通的DLL中维护对象并象COM一样发布对象的接口也是一件很简单的事情,没什么特别的,不过上面的讨论有一个很大的问题:如果我们的DLL只有一个导出函数,这意味这它只能导出一个对象的接口,就象上面那样,但如果我们要在这个DLL中维护多个对象怎么办呢(特别是一些按照继承关系连接起来的对象家族,或者具有共同特点的对象)?
※使用工厂模式:
解决上面问题的最好办法是在DLL的设计中使用工厂模式来管理其中维护的多个对象,这样做不仅可以维护不同的对象,还可以维护一个类的多个实例。然后我们只用在那个唯一的导出函数中导出这个工厂对象的接口,其它的对象接口都可以通过这个接口获得。比如象下面的样子:
function TFooManager.CreateAFoo: IFoo;
begin
inc(FooNum);
if length(FList)<FooNum then
setlength(FList,FooNum*2);
FList[FooNum-1]:=TFoo.Create;
…
result:=FList[FooNum-1] as IFoo;
end;
例如上面的TfooManager就是一个工厂类,它负责管理DLL中具体对象的生命周期,CreateAFoo创建一个DLL中名为Foo的对象,并把它保存到自己的私有字段,一个动态数组中:
private
FList:array of TFoo;
注意,上面的代码中用到了一个小小的技巧,每当flist的空间不够大时,我们采用了双倍分配策略,既在这个时候我们给flist它所需要的空间的两倍的空间。在创建对象请求频繁的时候,这无疑是一条有效的提高执行效率的策略,因为setlength函数在重新分配空间并依次移动已经存在的元素时是一个耗费时间的过程。这个策略也是一个典型的空间换时间的算法。
现在有了工厂模式后,我们要在dll中管理多个对象或则不同的对象就十分方便了,我们甚至可以让CreateAFoo接受一个参数以确定创建何种类型的对象,并且每新增加一种对象我们就在TfooManager工厂类中添加一个对象的私有字段(动态数组,当然也可以根据你的需要使用其它的数据结构如链表TList)。关于这个工厂类的其它方法的代码请参看文后的代码清单。
问题和解决技巧:
※引用计数问题(当我们需要手工管理对象的生命周期时):
既然我们在工厂类中定义了私有字段以存贮dll中的诸多对象实例,那从这里得到的一个很显然的观点是我们需要手工管理其中对象的生命周期,我们也许还需要在工厂类中添加一个用于释放所管理对象的方法:
procedure TFooManager.DelAFoo(id:integer);
var
i:integer;
begin
if FooNum>0 then
begin
FList[id].Free;
for i:=id to FooNum-2 do
begin
FList[i]:=FList[i+1]; //移动剩余元素,使对象在flist中保持连续存贮。
end;
FList[FooNum-1]:=nil;
Dec(FooNum);
end;
end;
这个方法根据传入的一个ID值释放指定的对象(在管理多种不同的对象时,还需要接收一个代表所需要释放对象类型的参数以决定释放哪种类型的对象)。好,一切看上去都很正常,但当我们从DLL外在调用工厂方法创建对象的时候问题出现了:
procedure CallCreateFoo;
var
tempfoo:IFoo;//要创建的Foo对象的接口IFoo
begin
tempfoo:=FooMan.CreateAFoo; //FooMan是工厂类的对象的接口
end;
当我们需要在这个过程外使用刚才创建的对象时(例如我们可以使用工厂类的一个方法TFooManager.GetFooByID(id: integer)根据传入的id找到flist中指定的对象,如刚才我们通过工厂方法创建的那对象),会出现一个内存访问错误,仔细观察后会发现我们刚才创建的对象根本不在内存中!为什么?由于delphi中的接口都继承自Iinterface,这是一个与Iunknown一样的接口,这意味着我们的对象都必须实现Iunknown中的那3个方法,更进一步的我们为了不用手工书写这些代码我们的类都继承自默认实现了Iinterface的类TinterfacedObject。然而错误的根源就在这个地方,继承自TinterfacedObject的对象根本不允许我们手工管理它的生命周期,因为Iunknown的实现类会根据对象中的引用计数来维护对象的生命周期,而上面的tempfoo是一个过程的局部变量,当它离开作用域时会被delphi编译器自动调用tempfoo._ Release(这是Iunknown的方法),这个方法在引用计数为0的时候将自动释放对象!而我们在调用TFooManager.CreateAFoo方法时其中仅做了一次as操作result:=FList[FooNum-1] as IFoo;(delphi会在这时自动增加一个对象的引用计数),所以引用计数为1在_ Release后变为0,于是在我们想访问我们创建的对象之前这个对象就已经不存在了。好了,问题的起因弄的很清楚了,要解决也不难,我们只用在返回请求接口之前进行一次_AddRef操作增加引用计数值,这样除非我们手工释放对象,否则引用计数都不会为0,如下的改进:
function TFooManager.CreateAFoo: IFoo;
begin
…
FList[FooNum-1]:=TFoo.Create;
(FList[FooNum-1] as IUnKnown)._AddRef;
result:=FList[FooNum-1] as IFoo;
end;
好了,似乎我们已经克服了所有的困难,是这样吗?不是,麻烦马上又出现了!当我们这个时候调用工厂类的DelAFoo方法时会抛出更多的异常!。当我们释放对象的时候会调用到TinterfaceObject的free方法,在调用这个方法前编译器会自动调用TInterfacedObject.BeforeDestruction方法(事实上这是一个从Tobject继承下来的方法,但在Tobject中它没有任何的实现),这个方法代码如下:
procedure TInterfacedObject.BeforeDestruction;
begin
if RefCount <> 0 then
Error(reInvalidPtr);
end;
看到这里问题明白了吧,由于我们手动增加了引用计数值,使那个值在释放对象前也不会为0,而上面的代码在引用计数值为0的时候会抛出一个异常。解决这个问题的办法很简单,我们只需要再自己定义一个我们的TinterfacedObject类TourInterfacedObject,在这个类中复写(override,因为这是一个虚函数)BeforeDestruction,让它的代码部分空白:
procedure TInterfacedObject.BeforeDestruction;
begin
end;
然后我们DLL中所有的类只用从TourInterfacedObject继承就可以了。
※改进和手工管理
这次我们再进行测试时就没有任何的问题了吗?如果只是上面的代码的确没有问题了,但问题是我们可能需要在任何地方使用接口操作我们所管理的对象,而delphi编译器会在接口变量离开作用域或者被手工设置为nil时自动调用_IntfClear以决定是否释放实现接口的对象,如果在这之前我们已经调用诸如DelAFoo这样的方法手工释放了我们的对象,那么在调用_IntfClear时,一个访问异常出现了,因为这时对象已经根本不存在了!!现在是应该彻底把对象生命周期交给我们管理而不是交给接口管理的时候了(上面的做法是片面的,因为事实上对象内部仍然存在着一个引用计数值,我们只是用了一点技巧混淆了它对对象生命周期的管理),看来我们又要回到最开始了,我们不得不考虑不要TinterfacedObject而是自己写一个实现Iinterface的类,彻底的抛弃引用计数,并省略到诸如BeforeDestruction之类的不必要的方法:
TMyInterfacedObject = class(TObject, IInterface)
protected
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
function TMyInterfacedObject._AddRef: Integer;
begin
result:=1; //我们已经不需要引用计数了
end;
function TMyInterfacedObject._Release: Integer;
begin
result:=1;
end;
function TMyInterfacedObject.QueryInterface(const IID: TGUID;
out Obj): HResult;
begin
if GetInterface(IID, Obj) then
Result := 0
else
Result := E_NOINTERFACE;
End;
很高兴的告诉大家,到此我们已经解决了对象生命周期管理中的所有问题,我们所管理的对象可以正常工作了!而且我们还可以去掉上面的技巧性代码,从这个TmyInterfacedObject继承就已经足以解决所有的问题了。也许有人可以看到上面的代码已经极大的破坏了COM规范,然而这正是本文的目的(J),通过我们的改进,我们手工管理的对象工作的很好,而且这也正是我们所需要的不是吗?