Delphi之适配器模式(Adapter)

作者 陈省

 

适配器模式可以将类的接口转变成客户需要的其它的接口。这样做的好处就是很好的重用已有的代码,比如很多的软件公司都生产数据库产品,如Oracle,IBM,Microsoft,Borland,他们的产品各有各的特点,原生的开发方法也有很大的区别,比如Oracle就提供有OCI接口,Borland提供了InterBase Api,Sql Server也有自己的专用Api。如果都使用数据库厂商的原生开发方法,那么在Sql Server开发的软件就很难移植到其它的数据库平台上去,这无疑会限制产品的应用范围。那么为此很多的厂商就提供了各种的标准的开发接口,比如最早的ODBC,BDE,到最近的Ado.net以及DBExpress,有了这种标准的数据库开发接口,我们在一个平台上开发的产品就很容易移植到其它的平台上。那么如何让数据库适应不同的标准Api而无须大的改动呢,这就是通过适配器模式来实现。最早的ODBC Driver和现在Ado.net Driver以及DbExpress Driver就相当于一个适配器可以让数据库适应不同的标准开发接口,这样原有的产品无须针对标准的Api做任何改变,只要针对不同的开发Api实现一个Driver就可以了。

 

适配器模式的实现原理很简单,就是通过实现了客户指定接口的类来响应客户的请求,然后在适配器内部将请求转发给原有的Api接口的对应的具有类似功能的方法,然后处理后的结果整理后返回给客户。

 

Vcl中的适配器模式

 

在Delphi中,TStream类定义了Vcl中的流机制的接口,所有Vcl中的流类,文件流TFileStream,内存流TMemoryStream都是从TStream类继承出来的,TStream类定义的函数可以很好的满足Vcl组件的存取流的需求。但是,在COM中微软进行的流的存取都是通过IStream接口来实现的,但是TStream在最早设计时并没有实现IStream接口,但是TStream类本身已经实现的功能可以满足IStream的接口的需要,因此Delphi的研发人员为了在不改变TStream类的实现的情况下可以重用TStream类来满足COM的流机制的要求,定义了TStreamAdpter类(定义在classes.pas单元中)。这个类实现了IStream接口,当COM客户端调用IStream接口的函数时,TStreamAdpter类将请求都转发给内部的一个TStream的实例,由TStream的实例来完成请求的操作。

 

Vcl中的TImageList组件是对windows提供的标准的COM组件ImageList的封装,在windows中图像列表数据都需要通过IStream接口来读取,下面就是Vcl中使用TStreamAdapter来完成COM数据读取的例子:

 

procedure TCustomImageList.ReadD3Stream(Stream: TStream);
var
  LAdapter: TStreamAdapter;
  LTemp: TMemoryStream;
  LRetry: Boolean;
  LValue, LBitCount: Byte;
begin
  LAdapter := TStreamAdapter.Create(Stream);
  try
    Handle := ImageList_Read(LAdapter);
  finally
    LAdapter.Free;
  end;
  
end;

 

TStreamAdpater的构造函数需要一个已经创建好的TStream对象实例作为参数,构造时将TStream实例保存起来,之后COM组件的所有对IStream接口的请求都会转发给这个实例,当ImageList_Read方法调用完毕后,数据就被读入到了TStream中,之后我们就可以使用熟悉的标准的TStream函数来进行处理。

 

上面的TStreamAdpter可以用于向外部的COM组件提供一个IStream接口,那么有的时候呢,我们又需要用外部的COM组件提供的IStream接口中读写数据,对于这种情况呢,Vcl中提供了另外一个继承自TStream的TOleStream类来完成将IStream接口适配成TStream的功能。用法非常简单,只要将外部COM组件提供的IStream接口作为参数调用构造函数来创建TOleStream的实例,我们就可以像操作普通的TStream那样操作IStream接口了:

 

procedure TActiveXControl.SaveToStream(const Stream: IStream);
var
  OleStream: TOleStream;
  
begin
  OleStream := TOleStream.Create(Stream);
  Try
    //写入数据的操作,以下代码省略
  finally
    OleStream.Free;
  end;
end;

 

类适配器和对象适配器

 

上面的TStreamAdapter适配器是一个独立的类,它将所有对IStream接口的调用包装后转发给TStream的实例来完成,TStreamAdapter和TStream的关系是对象组合的关系,一般称这类使用对象组合来实现的适配器为对象适配器。对象适配器的UML图示意如下:

 

除了对象适配器外,适配器还可以通过多重继承来实现,使用多重继承实现的适配器通常称为类适配器。

 

在Delphi的Source\Toolsapi目录下的IStreams.pas单元中,定义了派生自TStreamAdapter的类TIStreamAdapter,这类除了IStream接口外还实现了IStreamModifyTime接口,IStreamModifyTime的接口定义如下:

 

  IStreamModifyTime = interface
    ['{12452621-5F8A-11D1-9FB6-0020AF3D82DA}']
    function GetModifyTime: Longint; stdcall;
    procedure SetModifyTime(Time: Longint); stdcall;
  end;

 

IDE通过该接口的方法可以存取流数据变更的时间。

 

function TIStreamAdapter.Write(pv: Pointer; cb: Longint;
  pcbWritten: PLongint): HResult;
begin
  Result := inherited Write(pv, cb, pcbWritten);
  FModifyTime := DateTimeToFileDate(Now);
end;

 

procedure TIStreamAdapter.SetModifyTime(Value: Longint);
begin
  FModifyTime := Value;
end;

 

其中FModifyTime是类的私有变量,用来保存变更时间的。上面的类适配器的UML图示意如下:

 

 

类和对象适配器的应用场景

 

适配器有不同的实现方式,一般来说当以存在的接口同要适配的接口相似程度比较大的话,可以考虑采用对象适配器。而当接口差异比较大的时候,由于对象适配器是通过对象组合来实现接口适配,重定义已有接口比较困难,这时就可以考虑使用类适配器。

 

一般来说,应该尽量将适配器实现为对象适配器,这样适配器可以适配已有的类及其所有的子类,而类适配器由于采用了继承的方式,则需要重载父类的虚方法,对父类的默认实现有所改变,则通常它只能适配它的父类,而对于它的父类的其它子类则无法适配,有应用的局限性。

 


本站原创及翻译内容保留版权,欢迎转贴,转贴时请注明转自Delphi深度探索