delphi(6)
时间:2006-06-10 来源:许我一个信仰
第7章 抽象类和静态接口
在COM协议实现之前,抽象接口就已经存在了。抽象接口是面向对象术语,它是一个未实现的接口,但标识了该接口所有可能的实现。理解抽象接口的一般工作原理有助于设计和实现更好的软件体系结构,它也是理解COM用法的关键所在。
静态接口指的是类方法。类方法与C或C++中的静态方法等价。静态方法存在于类的层次上。无须特定的对象即可调用类方法。常见的体现了类方法行为的例子是构造函数,它是用于创建类实例的特殊方法。除了对象的构造,类方法还有其他用途。在Delphi中,将抽象接口和静态方法结合起来就可以很容易的实现瘦客户以及DLL应用程序,这对于使用COM和DCOM进行分布式程序设计是个很好的预习。
本章中,您将学会如何实现抽象接口,创建进程内动态链接库,以及从DLL向应用程序高效地传递对象。
7.1 类方法的实现
大多数方法都具有实例作用域。对于定义在类中的数据,每个类都有自己的副本。在方法调用前实例必须已经存在,而且语法要求把对象用点操作符(.)连接到方法作为调用的前缀(除非使用with object do结构)。这对于类方法以外的所有方法都是对的。
类方法具有类作用域。它们的行为与C或C++中的静态方法几乎完全相似。尽管可以用实例来调用类方法,但只需将类名用的操作符连接到类方法即可。类方法的语法与其他方法相同,但需要把class关键字作为方法声明的第一个词。
class methoddeclaration
例如class Procedure Foo;,无论定义在任何类中都表示Foo是个类过程。给出class TBoolean,将布尔值转换为字符串值的类方法可如下实现。
type
TBoolean = class
public
class Function BooleanToString( const Value : Boolean ) : String;
end;
implementation
class function TBoolean.BooleanToString(const Value: Boolean): String;
const
BOOLS : array[Boolean] of string = ('False', 'True');
begin
result := BOOLS[Value];
end;
该类方法是个函数,当给出布尔值时返回对应的字符串。调用该方法,只需使用类名和类方法名。
MessageDlg( TBoolean.BooleanToString( False ), mtInformation, [mbOK], 0);
警告:对未初始化的对象调用非类方法,将导致访问违例异常EAccessViolation。
上面的代码显示了一个消息对话框,其中文本为‘False’。从类方法的实现中可以看到,布尔值被用作字符串数组的索引,以返回字符串。重要的是要注意到无须对象即可调用类方法。所有的方法都是类方法的类可用于定义无状态类,即没有数据的类。
7.1.1 创建无数据类
按照通常的规则,没有数据的类意味着其方法无法组成一个类,而需要合并到已存在的类中。就像大多数规则一样,例外总是存在的。数据库类就是作为工具类而发明的,另一个常见的例子是包含成百上千算法的数学库,可以将其组织在整洁的由类构成的包中。
实用工具类没有数据,因为数据意味着状态,此外需要对象存在。完全版的TBoolean类就是一个无数据类的例子。
unit UBoolean;
interface
uses
SysUtils;
type
TBoolean = class
public
class function BooleanToString( const Value : Boolean ) :
String;
class function StringToBoolean( const Value : String ) :
Boolean;
class function BooleanToYesNo( const Value : Boolean ) :
String;
class function YesNoToBoolean( const Value : String ) :
Boolean;
end;
implementation
{ TBoolean }
class function TBoolean.BooleanToString(const Value: Boolean): String;
const
BOOLS : array[Boolean] of string = ('False', 'True');
begin
result := BOOLS[Value];
end;
class function TBoolean.BooleanToYesNo(const Value: Boolean): String;
const
YES_NO : array[Boolean] of String = ('No', 'Yes');
begin
result := YES_NO[Value];
end;
class function TBoolean.StringToBoolean(const Value: String): Boolean;
begin
result := CompareText( Value, 'True' ) = 0;
end;
class function TBoolean.YesNoToBoolean(const Value: String): Boolean;
begin
result := CompareText( Value, 'Yes' ) = 0;
end;
end.
SysUtils.pas单元中加入了大小写敏感的CompareText函数。上面的类由StringToBoolean、YesNoToBoolean和BooleanToYesNo组成,完成了逻辑类型与字符串之间的转换。定义实用工具类提供了一个整洁的包,并且可以随着时间进行扩展。
继承静态类
静态类是只有类方法的类。假定您实现了一个静态类,如TBoolean,需要随时间而扩展以包括其他所需要的转换功能,可以使用子类化来扩展行为;否则就需要重新编译所有使用TBoolean类的代码。另一方面,子类化是一种方便的途径,它可以扩展布尔值转换能力而无须影响已存在的代码。
uses
UBoolean;
type
TConvert = class(TBoolean)
public
class function AnyStringToBoolean( const Value : String ) :
Boolean;
end;
implementation
class function TConvert.AnyStringToBoolean( const Value : String ) :
Boolean;
begin
result := (Length(Value) > 0 ) and (Value[1] in ['T', 't', 'y', 'Y']);
end;
提示:按照惯例,把类名中的T前缀改为U前缀来作为单元名,可以很容易地找到包含特定类的单元。上面列出的代码中,很容易辨认出UBoolean是包含TBoolean类的单元。
TConvert类也是一个IsA TBoolean类,它继承了所有TBoolean的类方法,并实现了AnyStringToBoolean方法。新的类方法确保输入的字符串长度大于零,并通过检查第一个字符是否是T、t或Y、y来验证True和Yes。在该实现中,所有其他的字符串都作为False对待。由于TConvert只包含类方法,因此并不需要TConvert的实例。
聚合静态类
如果要通过子类化扩展多个类的行为,也可以使用聚合。前面的例子中,TBoolean听起来仿佛是TConvert类的子类,而不是反过来那样。这样,语义听起来像是错的。按照规则,应尽可能使语义正确,但静态类和工具类确实在规则之外。但如果您不喜欢TConvert类与TBoolean类之间IsA的语义,您可以使用聚合来实现TConvert和TBoolean类的关系,即HasA关系。
type
TBooleanClass = Class of TBoolean;
TConvert = class
public
class function BooleanClass : TBooleanClass;
class function AnyStringToBoolean( const Value : String ) :
Boolean;
end;
implementation
class function TConvert.AnyStringToBoolean(const Value: String): Boolean;
begin
result := (Length(Value) > 0 ) and (Value[1] in ['T', 't', 'y', 'Y']);
end;
class function TConvert.BooleanClass: TBooleanClass;
begin
result := TBoolean;
end;
现在两个类之间是包含关系。可如下使用TConvert类调用TBoolean类中的静态方法。
if( TConvert.BooleanClass.StringToBoolean( 'False' ) = False ) then
// … do something
这有些稍许冗长,但仍无须改变依赖于TBoolean的代码,而且扩展了TBoolean类的行为。如果需要在使用聚合时像继承一样方便,可以在TConvert类的接口中定义与TBoolean相同的类方法,并用TBoolean来实现这些方法。
type
TConvert = class
public
class function YesNoToBoolean(const Value: String): Boolean;
class function AnyStringToBoolean( const Value : String ) :
Boolean;
end;
implementation
class function TConvert.YesNoToBoolean(const Value: String): Boolean;
begin
result := TBoolean.YesNoToBoolean( Value );
end;
上述例子示范了接口提升的概念。TConvert类通过使用TBoolean类的方法实现了YesNoToBoolean行为。可以看到,现在有了多个选择。关键在于把已存在的行为扩展到新的领域而无须影响或重新测试以存在的代码。继承、聚合以及利用聚合关系实现提升接口等途径,无论对于静态类还是常规类都是可行的。
7.1.2 构造函数和析构函数
构造函数是用于创建并初始化对象的特别方法。构造函数使用关键字constructor。
constructor Create;
注意:上一节中提到的静态类,是不需要构造函数和析构函数的,因为并不需要创建其实例。但它们确实从TObject继承了所有的方法,包括构造函数和析构函数。
构造函数与函数的类方法密切相关。您可以定义类函数模拟构造函数的行为,并返回对象的实例。
class function TFoo.Create : TFoo;
begin
result := inherited Create;
end;
procedure TFoo.Hello;
begin
ShowMessage('Hello World!');
end;
使用变量Foo : TFoo,调用Foo := TFoo.Create看起来与构造函数非常相似,但实际调用了静态方法Create,该方法调用从TObject继承的构造函数。类函数TFoo.Create示范了构造函数的行为。构造函数具有特别的语义“首先调用构造函数以进行对象初始化”,所以可定义特定的构造函数来表示特定的语义。要定义构造函数,只需添加一个方法,并用关键字constructor代替通常的procedure或function。按照惯例,构造函数命名为Create。可以传递任意数目的所需参数来初始化对象。几个构造函数的例子如下。
constructor Create;
constructor Create(AOwner : TComponent);
constructor Create( const FileName : String; Mode : Word );
第一个构造函数没有参数。第二个构造函数有一个TComponent类型的参数,名为AOwner(第二种形式是组件的构造函数)。第三种形式有一个常量字符串参数,名为FileName,以及一个Word类型的参数,名为Mode。第三个构造函数是TFileStream类的构造函数的形式。
析构函数使用关键字destructor,需要使用对象实例才能调用,它不是静态方法。关键字destructor是一个特别的结构,意为“最后调用析构函数,以进行对象的清除工作”。按照惯例,析构函数命名为Destroy,无须参数。析构函数通常如下定义。
Destructor Destroy; override;
因为每个类都是由TObject子类化而来,而TObject已经定义了一个虚析构函数,因此要重定义析构函数,必须使用override指令。
7.2 维护无对象状态
Java和C++支持静态特性。Delphi并不直接支持静态数据,但在没有对象的类中可以实现静态数据的等价形式。
type
TNoObject = class
public
class Function Data( const NewValue : Integer = 0;
const Change : Boolean = False ) : Integer;
end;
implementation
class function TNoObject.Data(const NewValue: Integer;
const Change: Boolean): Integer;
const
{$J+}FData : Integer = 0;{$J-} // make writable
begin
if( Change ) then FData := NewValue;
result := FData;
end;
上面的类定义了一个类函数,在函数内部存储了一个可写常数。通过定义两个默认参数,该函数可以像数据一样用作右值。要将该静态数据作为左值使用,需要向函数传递两个参数,这有一点麻烦。
TNoObject.Data( 7, True );
while( TNoObject.Data > 0 ) do
begin
ShowMessage( IntToStr( TNoObject.Data ));
TNoObject.Data( TNoObject.Data - 1, True );
end;
用这种方式管理数据过于笨重,但可以有效地实现静态数据。下面列出的代码示范了如何实现静态的StringList。
type
TStaticStrings = class
public
class Function Strings( const Cleanup : Boolean = False ) :
TStrings;
class Procedure Finalize;
end;
class function TStaticStrings.Strings(const Cleanup: Boolean):
TStrings;
{$J+}
const
FStrings : TStrings = Nil;
{$J-}
begin
if( Cleanup ) then
begin
FStrings.Free;
FStrings := Nil;
end
else
begin
if( FStrings = Nil ) then
FStrings := TStringList.Create;
end;
result := FStrings;
end;
class Procedure TStaticStrings.Finalize;
begin
Strings( True );
end;
在上述的TStaticStrings类中,如果内含的TStrings对象为nil,将自动对其进行初始化。在其他对象可以使用的右值环境中,也可以使用TStaticStrings.Strings类方法。调用Finalize可以清除动态的字符串列表对象,它调用了Strings方法,并向Cleanup参数传递True值。
7.3 动态链接库编程
动态链接库通常用于存放过程。许多松散相关的过程包含在DLL中,它们都是独立的过程,并不构成更大结构的一部分。近来,DLL已经用作进程内OLE自动化和COM服务器的容器。在这两种用途之外,DLL还可以作为进程内应用程序,内含通常所称的业务对象。
Delphi可以在动态链接库中实现类,并在使用该DLL的程序中使用这些类,使得能够创建进程内瘦客户和服务器应用程序。由于动态链接库可用于存储通用工具函数、业务对象服务器以及COM应用服务器等,本节涵盖了如何在Delphi中创建并使用动态链接库。
7.3.1 调用DLL过程
在进程将DLL装载入内存后,该DLL导出的过程可以像其他过程一样使用。DLL运行在装载DLL的应用程序的进程空间中,并且对所有使用该DLL的应用程序共享同一代码副本。如果在某个过程声明中包括了external子句和DLL的名字,在应用程序开始运行的时候,程序将试图在Application.Initialize方法运行前装载对应的DLL。如果使用动态的DLL装载方法,当调用LoadLibrary时即可装载DLL。动态装载可以使程序员更好地控制程序,并使应用程序启动更快。而静态或隐式装载DLL则更为容易,因为Windows完成了大部分的工作。
隐式DLL装载
出于示范的目的,我们假定存在一个DLL,其中包含了联系信息:名字、电话号码和电子邮件地址。客户程序无须关心DLL如何实现,从DLL用户的角度看来,所需的信息只是导出过程的名字和声明。示例DLL导出了五个过程:AddContact、RemoveContact、CountContacts、Initialize和Finalize。要求Initialize必须首先调用,当DLL不需要再存储数据时调用Finalize。
注意:给出的过程代表了结构化程序设计的风格。建议您不要使用这种编程风格,这里的目的只是为了示范如何隐式装载DLL。
该DLL所存储的联系信息项定义为紧缩记录TContact,其中包括Name、Phone和EMail字段,都是字符串。
要使用这五个DLL方法,只需在客户应用程序中声明它们。
Procedure AddContact( Contact : TContact ); external 'ContactServer.dll';
Procedure RemoveContact( Phone : String ); external 'ContactServer.dll';
Function CountContacts : Integer; external 'ContactServer.dll';
Procedure Initialize; external 'ContactServer.dll';
Procedure Finalize; external 'ContactServer.dll';
在应用程序中,external声明可以定义在任意单元的接口或实现部分。如果声明在实现部分,它们只对声明的单元本地是可用的。如果声明在接口部分,则任何包括了声明单元的单元都可以访问它们(后者是Delphi用来把Windows API导入到Window.pas和messages.pas单元中的方法,可以透明地调用这些过程而无须知道是在使用Windows API)。
使用external子句声明过程,将使得应用程序运行时立即装载DLL并解析该过程的地址。当DLL不再使用时,Windows将负责卸载ContactServer.dll。下面列出的代码示范了使用DLL过程的例子。
var
Contact : TContact;
begin
Initialize;
try
Contact.Name := 'Paul Kimmel';
Contact.Phone := '(517) 555-1212';
AddContact( Contact );
Contact.Name := 'Frank Arndt';
Contact.Phone := '(517) 555-2121';
AddContact( Contact );
ShowMessage( IntToStr( CountContacts ));
RemoveContact( '(517) 555-2121' );
ShowMessage( IntToStr( CountContacts ));
finally
Finalize;
end;
end;
注意:使用DLL的好处在于许多应用程序可以同时使用同一DLL,而用户无须关心DLL的实现细节。在上例中,TContacts记录的存储是无序的。
隐式装载的好处是比较容易,而且Windows会管理DLL。而动态装载较为困难,需要使用过程类型得到DLL过程的地址。在上一节中您已经学过这个,不会有任何问题。
动态DLL装载
当由程序员决定何时装载DLL时,称DLL是动态装载的。调用定义在kernel32.dll中的API函数LoadLibrary即可动态装载DLL,并存储服务器的句柄。动态装载的库在使用完毕之后必须释放,以LoadLibrary返回的句柄为参数调用FreeLibrary即可。在装载与释放库之间,要使用动态装载的DLL,需要调用GetProcAddress得到DLL中过程的地址,并将该地址赋值给具有正确的过程类型的变量。通过过程类型的变量才能调用DLL过程。
按照需要装载库,可以使应用程序更快地启动并使用更少的内存,可称之为惰性实例化。可以在需要DLL的功能时载入它。如果不需要,就不必装载对应的库,因为应用程序的每次运行并不都使用所有的功能。
使用上一节中联系信息的例子,动态载入库的代码版本如下。
var
AddContact : procedure( Contact : TContact );
RemoveContact : procedure( Phone : String );
CountContacts : function : integer;
Initialize, Finalize : procedure;
function SafeGetProcAddress(hModule: HMODULE;
lpProcName: LPCSTR): FARPROC;
begin
result := GetProcAddress( hModule, lpProcName );
if( result = Nil ) then Abort;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Handle := LoadLibrary( 'ContactServer.dll' );
try
AddContact := SafeGetProcAddress( Handle, 'AddContact' );
RemoveContact := SafeGetProcAddress( Handle, 'RemoveContact' );
CountContacts := SafeGetProcAddress( Handle, 'CountContacts' );
Initialize := SafeGetProcAddress( Handle, 'Initialize' );
Finalize := SafeGetProcAddress( Handle, 'Finalize' );
except
Application.Terminate;
end;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FreeLibrary( Handle );
end;
修订版本向客户程序的主窗体添加了OnCreate事件处理程序。该处理程序装载动态链接库,并将句柄存储在类中的变量Handle : LongWord中,LongWord是LoadLibrary的返回类型。每个导出的过程都存储在与其同名的本地变量中,这些变量都具有与对应的DLL过程相适应的过程类型。过程SafeGetProcAddress负责检查kernel32.dll中的API函数GetProcAddress返回值不是Nil。如果过程未成功装载,应用程序将退出。OnDestroy事件处理程序释放了库。
该实现模拟了隐式装载的方式,来从DLL中装载过程。但如果确实隐式装载DLL而失败,那是无法恢复的。应用程序可以编译通过,但运行时会失败,这是非常尴尬的事情。如果动态装载DLL,那么应用程序将可以编译并能够运行,它会在试图使用DLL的功能时失败,但您可以优雅地从中恢复。
7.3.2 编写动态链接库
当创建DLL时,有四项基本的工作需要完成。您需要编写实现DLL的代码。您需要定义导出的过程。您需要编写测试程序,对于测试程序,可以使用隐式装载方式,用external子句声明过程。您还必须解决在测试中发现的问题。请记住,编写进程内DLL的好处在于隐藏了DLL的实现细节,减少了使用该DLL的客户程序的实现复杂性,从而增加了重用的可能性。即,可能有多个程序使用该DLL。
您已经知道如何编写应用程序。创建DLL应用程序意味着需要创建库工程。可以选择菜单项File,New,Other,并单击DLL Wizard应用程序类型(如图7.1所示)。当由Delphi的主菜单选择Project,View Source菜单项时,可以看到.DPR文件的源代码。它与可执行文件稍有不同。
library Project1;
{ Important note about DLL memory management: ShareMem must be
the first unit in your library's USES clause AND your project's
(select Project-View Source) USES clause if your DLL exports
any procedures or functions that pass strings as parameters or
function results. This applies to all strings passed to and
from your DLL—even those that are nested in records and
classes. ShareMem is the interface unit to the BORLNDMM.DLL
shared memory manager, which must be deployed along
with your DLL. To avoid using BORLNDMM.DLL, pass string
information using PChar or ShortString parameters. }
uses
SysUtils,
Classes;
{$R *.RES}
begin
end.
创建工程后所需做的与其他的应用程序区别不大,如添加窗体、单元以及数据模块等。不同之处在于库不能作为单独的应用程序运行,而用户只能直接访问导出的过程。
定义exports子句
为简明起见,我们将使第一个DLL简单些。单击File,New,Other并双击DLL Wizard,创建一个新的库工程。在begin和end.块语句(库中第一个执行的块语句)之前添加下列代码。
Procedure HelloFromServerLand;
begin
ShowMessage('Hello from server land');
end;
exports
HelloFromServerLand;
图7.1 选择File,New,Other菜单项并单击图中所示的
DLL Wizard按钮,即可创建DLL应用程序
向库的uses子句添加Dialogs,该单元包括了ShowMessage过程。exports子句表示只导出HelloFromServerLand,它是该DLL的用户所能调用的惟一过程。按下列步骤测试DLL。
1.单击View,Project Manager菜单项,显示工程管理器,如图7.2所示。
图7.2 在Project Manager视图中可以看到,工程组
中添加了一个DLL工程和一个可执行工程
2.单击ProjectGroup1,并单击Project Manager对话框中的New按钮。
3.从如图7.3所示的New Items对话框中选择Application。
图7.3 在Project Manage中双击New 所打开的New Items对话框,
也可从Delphi的主菜单中选择File,New,Other打开
4.从project2.exe工程下所列出的文件中选择Unit1。
5.打开代码编辑器,添加external声明,以装载‘Project1.dll’库(默认命名)并导入HelloFromServerLand过程。
procedure HelloFromServerLand;external 'Project1.dll';
6.在Project Manager视图中,选定Project2.exe并双击该应用程序。
7.双击可执行应用程序的Form1窗体以创建TForm1.FormCreate事件方法并在begin和end块之间键入:
HelloFromServerLand;
8.确认该可执行应用程序,即测试程序,是当前选定的应用程序,按键F9运行程序。将显示简单的对话框,其中有文字“Hello from server land”。
在这8个步骤中,您创建了DLL并进行了测试。余下的问题是实现一些有用的行为。关于瘦客户编程的章节涵盖了更多实质性的主题。本节余下的部分涵盖了DLL编程的机制。
库的初始化代码
与可执行应用程序的DPR源代码文件相似,库工程的源文件也包含begin和end块语句。在主块语句的begin与end之间可以放入任何用于初始化库工程的代码。主块语句与C或C++应用程序的main过程或非Delphi编写的应用程序中的LibMain过程相似。
根据经验,要保持initialization部分相对简单。按照库的需要,单元、窗体和数据模块的数目是不限的,而每个模块都有initialization和finalization部分。每个单元的initialization和finalization部分都在库的begin与end块语句之前运行。因此在需要初始化的单元中,可以可靠地执行单元级的初始化。因此要保持库的初始化代码简单。
禁止使用全局变量
Delphi应用程序不能导入全局变量。全局变量通常不是好主意,这一规则对于DLL和普通的应用程序都是适用的。在DLL和客户应用程序之间传递数据的最好的方法是使用类。可以越过模块边界传递对象。对于像记录和过程类型这样的简单类型,可以在单元中声明该类型并在客户和DLL中都包括该单元,这样就可以越过模块边界共享该类型的变量。
在有关隐式装载DLL的一节中,ContactServer.dll接收了TContact紧缩记录类型的变量。但仍然不能跨越模块边界共享全局数据,只能越过模块边界得到数据。最好通过DLL中定义的过程来完成这项工作。关于如何实现实用的DLL以及从DLL向应用程序传递对象,请阅读有关瘦客户程序设计的章节。
7.3.3 处理DLL异常
动态链接库过程所引发的异常如果没有在DLL中进行处理,将会跨越模块边界,可以在调用该过程的应用程序中使用try except或try finally块语句进行处理。
Procedure RaiseDLLExceptionProc;
begin
raise Exception.Create('Raised in the DLL!');
end;
exports
RaiseDLLExceptionProc;
创建一个新的库工程并添加上述代码。为测试该异常,向工程组添加一个简单的测试程序并声明外部过程RaiseDLLExceptionProc。当在应用程序中调用RaiseDLLExceptionProc过程时,将引发异常,而可执行应用程序可以对其进行响应(见图7.4)。向测试程序添加如下代码。代码将调用DLL过程并示范了在客户程序中捕获DLL产生的异常(ShowException代表了Delphi对于未处理的异常所提供的默认行为)。
图7.4 DLL过程RaiseDLLExceptionProc所引发的异常
try
RaiseDLLExceptionProc;
except
on E : Exception do
ShowException( E, ExceptAddr );
end;
可以使用与可执行文件相同的原则来编写和处理DLL中的异常。对于DLL中的错误处理,异常是谨慎而正确的选择。
7.3.4 对字符串参数使用共享内存管理器
Delphi中的字符串并非零结尾的ASCII字符串。零结尾的ASCII字符串,第一个字符位于位置0,以字符0结尾。Delphi自动进行动态字符串管理,并在字符串文字的第一个字符位置之前包含了一些额外的数据。
Function MyLength( const S: String ) : Integer;
asm
mov ecx,[eax-4]
mov result,ecx
end;
上述代码对于传递给参数S的字符串值,返回其长度。默认使用了寄存器调用规范,参数列表中的第一个参数存储在EAX(累加器)寄存器中。EAX表示了指向字符串的指针。指令mov ecx, [eax-4]读出字符串起始地址向下偏移4个字节处的32比特值。
图7.5 Delphi在字符串起始之前存储了额外的信息,如图所示,也可看上面的代码
提示:正如每个库文件开头的“重要注记”所提示的,使用字符串参数或嵌套字符串参数需要在uses子句中包括sharemem单元并将BorlandMM.dll与您的应用程序一同发布,否则需要对参数值使用PChar或shortstring类型。
如果调用需要PChar参数的方法,Delphi字符串与零结尾字符串是兼容的;如果在导出的过程中传递字符串参数,则需要包括sharemem.pas单元。在库和使用库的应用程序的uses子句中,Sharemem.pas都应是第一个声明的单元。您还需要将BorlandMM.dll(Borland内存管理器)与程序一同发布,sharemem.pas使用该内存管理器来支持Delphi的字符串参数。
7.3.5 创建工程组
当创建新的应用程序时,Delphi生成一个工程组。如果向工程组中加入了多个工程,当保存文件时Delphi会提醒您保存工程组。使用工程组,可以更容易编译、测试以及调试多个相互依赖的应用程序。例如,已经建立了上一节的库工程,如果要测试RaiseDLLExceptionProc工程,可以向其工程组添加一个应用程序工程。双击应用程序工程使其称为当前工程,并按键F9运行该应用程序。工程组中的DLL也会被编译。要更新一个工程组中的所有文件,可以在Project菜单中选择Build All Projects菜单项。
7.3.6 测试DLL
测试DLL与其他应用程序很相似。在将DLL合并到实际的应用程序产品工程组之前,较好的办法是建立轻量级的工作台,试验一下DLL的功能。
注意:为测试新的单元、模块或类,所用的测试代码或建立测试程序的过程,可称之为工作台测试(scaffolding)。
工作台测试的好处在于它提供了环境,使得可以把调试和测试的注意力集中到单一的单元、模块或类上。编写工作台时要注意,应该可以直接而方便地测试所有代码路径。另一个好处是,如果新增的代码无法以这种方式测试,那么就意味着存在相互依赖——即紧耦合关系,同时也说明新增的代码是不完全的。
组件是个很好的例子,其代码必须独立于最终使用它的代码。代码的聚合有助于完善组件,但通常情况下组件引用使用它的代码则是毫无道理的。上一条规则的例外是在TGrid组件中所示例的双向关联关系。栅格知道Columns的存在,而Columns也了解包含它的栅格的存在。无论在编写组件、实用工具模块还是对话框窗体,都可以把代码剥离其程序的上下文环境,看是否可以在简单的工作台程序中对其进行测试。如果发现该代码需要大量的应用程序代码来支持,可以重新考虑其接口。
编译器开关
当调试DLL时,使用Project Options对话框中的Compiler属性页上的所有运行时错误设置。这些设置包括范围检查、I/O检查、溢出检查等,还要选中Compiler属性页上所有的调试选项。当选中Compiler属性页上的Use Debug DCUs选项后,可以在调试时步进到VCL代码中(参见图7.6)。
当去除了DLL中所有的错误并准备分发时,请取消运行时错误选项和调试选项并对应用程序进行一次完全的编译。这将删除调试代码,并减小编译后的应用程序的大小。
使用编译器指令动态添加和删除测试代码
编写用于测试和调试的代码是代价很高的。可以使用条件编译器指令来自动添加或删除调试代码,看情况而定。可使用{$IFOPT}、{$ENDIF}条件编译指令和D+开关来包括用于调试的代码。而使用D-开关则关闭调试信息。调试代码应是只读的。即,它不应改变执行的代码路径,因为这会在切换调试状态时导致难于发现的发散行为。考虑下列例子。
图7.6 选中Project Options对话框中Compiler属性页上的
Use Debug DCUs选项。将可以在调试时跟踪VCL代码
var
Ratio, Numerator, Denominator : Integer;
begin
Denominator := 0;
Numerator := 10;
{$ifopt D+}
if( Denominator = 0 ) then
begin
// calculate approximation, log error or something
Ratio := 0;
end
else
{$endif}
Ratio := Numerator div Denominator;
ShowMessage( IntToStr(Ratio));
end;
列出的代码示范了如果不包括调试信息将改变代码路径的情况。如果关闭调试信息,则不会包括if then语句的第一部分。将用于调试的代码与应用程序代码分离,并把调试代码用条件编译指令包裹起来,这样才能得到正确的行为。
var
Ratio, Numerator, Denominator : Integer;
begin
Denominator := 0;
Numerator := 10;
try
Ratio := Numerator div Denominator;
except
on EDivByZero do
begin
Ratio := 0;
{$IFOPT D+}
// calculate approximation, log error or something
{$endif}
end;
end;
ShowMessage( IntToStr(Ratio));
end;
上面的修改显著地改善了代码质量。被零除的情况可以由异常处理程序捕获,因此不再需要多余的条件测试;而负责记录错误或执行其他合适操作的调试代码也可以独立于应用程序代码。因此,调试代码不会因为编译选项的状态而产生引入外部无关行为的潜在可能性。
7.4 瘦客户程序设计
瘦客户程序设计已经风行了好几年了。远在OLE变成ActiveX及其后的COM之前,Delphi就已经支持瘦客户程序设计了。瘦客户是一些应用程序,其外观与廉价的好莱坞布景类似。房子看上去是真的,但没有房间。瘦客户包含了更多的东西,但理想情况下它们只包含了用于把数据表示出来的必要的信息。用于提供逻辑行为的业务规则是与界面的行为相分离的,被包装在DLL中。其目的是尽可能的使改变客户端更方便,而无须重新实现DLL,甚至无须再次访问该DLL。在Internet时代,许多管理人员预期许多应用程序可能会重新实现,从而可以在Web上更为廉价对其进行访问。客户程序运行在桌面机上,而服务器程序则位于硬件更为精良的服务器机器上,因此可以运行得更快。
注意:看一下Citrix Systems等公司股票的历史性的价格,就可以很清楚地知道各家公司愿意在分布式计算上投资多少钱。Citrix公司有一个产品名为WinFrame,它可以取得瘦客户程序的瞬间快照并将其在广域网之间发送。WinFrame可以使一些旧的、缓慢的、单体式的程序看起来就像是分布式瘦客户程序一样。WinFrame的硬件服务器要花费数十万美元。
但如果开发者必须返回到结构化编程风格,才能创建业务对象服务器,那么随着新客户程序的潜力而来的实际获利,将被生产率的下降所抵消。幸运的是,不需要使用结构化的编程风格。现在有两种很好的选择,一种是旧的,另一种是新的。可以使用Delphi中的抽象接口从DLL向应用程序传递对象,Object Pascal对此具有内建的支持;或者使用微软的COM和DCOM协议。Delphi对两者都是支持的。在Delphi中,使用抽象接口或COM和DCOM是两种平行的技术,它们都提供了可跨越进程边界访问对象的手段。最大的不同之处是它们的实现方式,另外COM和DCOM在各种语言中均可使用,并不限于Delphi。COM和DCOM将在本书的后续章节中讨论,其中会使用一些实际的例子。本章的其余部分示范了如何定义服务器,以及跨越进程边界传递业务对象的引用。
7.4.1 使用类引用
类引用定义了变量为类的类型。其语法为TTypeClass = class of TType,其中TType是该类型变量所引用的类,而TTypeClass是新的类型。类似于TTypeClass的类可称之为元类。
type
TClass = class of TObject;
上面定义了新类型TClass。声明的TClass变量是对类的引用。通常编写的代码类似于Foo( AOwner : TComponent ),编译器认为参数是对象的引用。但如果声明类引用类型TButtonClass = class of TControl并定义方法Foo(AButtonClass : TButtonClass),则编译器认为参数并非是对象引用而是类引用,AButtonClass并非实例,它是类。考虑如下代码。
unit UClassOfDemo;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs,
StdCtrls, ExtCtrls, Buttons;
type
TButtonClass = class of TControl;
TForm1 = class(TForm)
RadioGroup1: TRadioGroup;
procedure RadioGroup1Click(Sender: TObject);
private
{ Private declarations }
Procedure CreateButton( ButtonClass : TButtonClass );
public
{ Public declarations }
Procedure OnClick( Sender : TObject );
Procedure ClearButtons;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.ClearButtons;
var
I : Integer;
begin
for I := 0 to ControlCount - 1 do
if( Controls[I] Is TButton ) or (Controls[I] Is
TSpeedButton)then
Controls[I].Free;
end;
type
TFudgeControl = class(TControl);
procedure TForm1.CreateButton(ButtonClass: TButtonClass);
var
AButton : TControl;
begin
AButton := ButtonClass.Create(Self);
try
AButton.Parent := Self;
TFudgeControl(AButton).Caption := ButtonClass.ClassName;
TFudgeControl(AButton).OnClick := OnClick;
AButton.SetBounds( 1, 1, 75, 25 );
except
AButton.Free;
end;
end;
procedure TForm1.OnClick(Sender: TObject);
begin
ShowMessage( Sender.ClassName );
end;
procedure TForm1.RadioGroup1Click(Sender: TObject);
const
BUTTONS : array[0..2] of TButtonClass = (TBitBtn, TButton,
TSpeedButton);
begin
ClearButtons;
CreateButton( BUTTONS[ RadioGroup1.ItemIndex ] );
end;
end.
窗体上添加了一组单选按钮。当单击单选按钮组时,代码将动态地创建对应类型的按钮(见图7.7)。在窗体类中有四个方法:事件方法RadioGroup1Click响应对单选按钮组中按钮的单击;OnClick事件方法将被分配给动态创建的按钮;ClearButtons方法负责将按钮数目保持为1。
图7.7 单击单选按钮,上述代码将动态创建对应类型的按钮
在公有接口中,类引用类型定义为TButtonClass = class of TControl。之所以使用TControl,是因为TSpeedButton是基于图形的控件,而TButton和TBitBtn具有相似的祖先。TControl是最接近的共同祖先。由列出的第一个方法开始,单选按钮组的单击事件处理程序使用按钮项的索引来判断请求创建哪一种控件。数组BUTTONS包含了三个元素,都是TButtonClass类型。该方法将首先清除窗体上已存在的按钮,并将结果按钮类型——类引用而不是对象引用传递给CreateButton作为参数。
可以注意到CreateButton并不关心要创建的类的类型,它只需知道该类为TButtonClass类引用即可。在参数ButtonClass中所传递的类引用可基于按钮类型调用正确的构造函数。由于AButton定义为TControl类型,其中Parent、Caption和OnClick等属性均为保护权限的方法,因此定义了TControl类型的别名(请回忆一下,保护权限的方法在其定义的单元中是可访问的。因此TFudgeControl使得可以访问保护权限的属性)。由于CreateButton是私有的,因此我们可以确信只有合适的类才会传递给该方法,这样代码就可以可靠地工作。结果在一个代码块中可以创建几种对象。
7.4.2 定义纯虚抽象类
纯虚抽象类的成员都是抽象的。纯虚抽象类不会创建实际的实例,它们只是为了定义子类中必须支持的方法而存在。纯抽象方法在方法声明的结尾处使用编译器指令virtual和abstract即可。
type
IAbstract = class
public
procedure NoImplementation; virtual; abstract;
end;
列出的代码定义了一个抽象类IAbstract,它有一个方法NoImplementation。实现部分将不会定义NoImplementation方法的代码。这里的惯例是用I前缀代替T前缀(COM也对接口使用I前缀命名惯例)。从代码中可知,IAbstract默认的从TObject子类化而来,而IAbstract的所有子类都至少要实现NoImplementation方法。
type
TSubClass = class(IAbstract)
public
procedure NoImplementation; override;
end;
implementation
procedure TSubClass.NoImplementation;
begin
// some code
end;
TSubClass是IAbstract的派生类,它的一个方法实现了NoImplementation。因为父类是抽象的,因此无须调用继承的方法。
抽象类的好处在于,可以从抽象父类派生任意所需数目的子类。通过声明IAbstract类型的变量而将对象实现为子类的类型,可以定义动态的代码,从而在运行时实现任意的子类。抽象类的这种使用方式与COM的工作方式是一致的,更为重要的是可以使用抽象类来定义接口,而在想要的任何地方实现该接口。例如,接口的实现可存在于另一个应用程序中。这对于使用Delphi本地代码和COM进行瘦客户编程都是关键之所在。
7.4.3 创建面向对象的DLL
在7.3.1节“调用DLL过程”中,我们使用了一个简单的联系信息管理器。为使代码更加健壮、易用,应将联系信息定义为类,并创建联系信息列表来存储并管理联系信息项。DLL并不能把数据传递给用户,但它知道所有的联系信息和联系信息列表(下一节定义客户应用程序)。
由于要创建瘦客户程序来管理联系信息管理器的数据输入,需要定义一个抽象接口,客户程序和DLL都将用到该接口。而联系信息类和列表的实际实现只定义在DLL中。
unit XContact;
// XContact.pas - Contains abstract implementation of a contact
and contact list.
// Copyright (c) 2000. All Rights Reserved.
// by Software Conceptions, Inc. Okemos, MI USA (800) 471-5890
// Written by Paul Kimmel
interface
uses
classes;
type
IContact = class; // forward declaration
IContactList = class;
TContactClass = class of IContact;
TContactListClass = class of IContactList;
IContact = class
protected
function GetEMail: string; virtual; abstract;
function GetName: String; virtual; abstract;
function GetPhone: String; virtual; abstract;
procedure SetEmail(const Value: string); virtual; abstract;
procedure SetName(const Value: String); virtual; abstract;
procedure SetPhone(const Value: String); virtual; abstract;
public
constructor Create( const Name, Phone, EMail : String );
virtual;
property Name : String read GetName write SetName;
property Phone : String read GetPhone write SetPhone;
property EMail : string read GetEMail write SetEmail;
end;
IContactList = class
protected
function GetList : TList; virtual; abstract;
function GetContact( Index : Integer ) : IContact; virtual;
abstract;
procedure SetContact( Index : Integer; const Value : IContact);
virtual; abstract;
function GetCount : Integer; virtual; abstract;
public
constructor Create; virtual;
procedure Add( Contact : IContact ); virtual; abstract;
procedure Remove( COntact : IContact ); virtual; abstract;
property Contacts[Index : Integer] : IContact read GetContact
write SetContact; default;
property List : TList read GetList;
property Count : Integer read GetCount;
end;
implementation
{ IContactList }
constructor IContactList.Create;
begin
inherited;
end;
{ IContact }
constructor IContact.Create( const Name, Phone, EMail : String );
begin
inherited Create;
Self.Name := Name;
Self.Phone := Phone;
Self.EMail := EMail;
end;
end.
在XContact.pas单元中定义了两个类型,分别是IContact和IContactList的类引用。这对于把实现类从DLL传递到进行调用的应用程序是必要的。一般的,抽象类只定义了接口,它由虚抽象方法组成而没有实际的数据。请注意virtual和abstract属性方法可用于表示数据(关于特性的更多知识请阅读第8章)。客户程序所能得到的关于联系信息和列表的惟一表示就是抽象类。
联系信息和列表的实现定义在UImpContact单元中。只有DLL才实现了这两个类。如果客户程序也实现了这两个类,使用DLL就没有什么好处了。
unit UImpContact;
// UImpContact.pas - Contains the implementation of contact and
contact list
// Copyright (c) 2000. All Rights Reserved.
// by Software Conceptions, Inc. Okemos, MI USA (800) 471-5890
// Written by Paul Kimmel
interface
uses
XContact, Classes, SysUtils;
type
TContact = class(IContact)
private
FEMail : String;
FName : String;
FPhone : String;
protected
function GetEMail: string; override;
function GetName: String; override;
function GetPhone: String; override;
procedure SetEmail(const Value: string); override;
procedure SetName(const Value: String); override;
procedure SetPhone(const Value: String); override;
public
property Name : String read GetName write SetName;
property Phone : String read GetPhone write SetPhone;
property EMail : string read GetEMail write SetEmail;
end;
TContactList = class(IContactList)
private
FList : TList;
protected
function GetList : TList; override;
function GetContact( Index : Integer ) : IContact; override;
procedure SetContact( Index : Integer; const Value :IContact); override;
function GetCount : Integer; override;
public
constructor Create; override;
procedure Add( Contact : IContact ); override;
procedure Remove( COntact : IContact ); override;
destructor Destroy; override;
property Contacts[Index : Integer] : IContact read GetContact write SetContact;
property List : TList read GetList;
property Count : Integer read GetCount;
end;
implementation
{ TContact }
function TContact.GetEMail: string;
begin
result := FEMail;
end;
function TContact.GetName: String;
begin
result := FName;
end;
function TContact.GetPhone: String;
begin
result := FPhone;
end;
procedure TContact.SetEmail(const Value: string);
begin
FEmail := Value;
end;
procedure TContact.SetName(const Value: String);
begin
FName := Value;
end;
procedure TContact.SetPhone(const Value: String);
begin
FPhone := Value;
end;
{ TContactList }
constructor TContactList.Create;
begin
inherited;
FList := TList.Create;
end;
destructor TContactList.Destroy;
begin
while( FList.Count > 0 ) do
begin
TContact(FList.Items[0]).Free;
FList.Delete(0);
end;
FList.Free;
inherited;
end;
procedure TContactList.Add( Contact : IContact );
begin
FList.Add( Contact );
end;
procedure TContactList.Remove( Contact : IContact );
begin
FList.Remove( Contact );
end;
function TContactList.GetContact(Index: Integer): IContact;
begin
result := TContact(FList.Items[Index]);
end;
function TContactList.GetCount: Integer;
begin
result := FList.Count;
end;
function TContactList.GetList: TList;
begin
result := FList;
end;
procedure TContactList.SetContact(Index: Integer; const Value: IContact);
begin
FList.Insert( Index, Value )
end;
end.
这里有几个令人惊异之处。关键是要记住,只有DLL才知道类是如何实现的。库的源代码也非常简单:导出两个函数,分别返回对TContactClass和TContactListClass的引用。将返回这些类的实现形式,而不是其抽象版本。
library NewContactServer;
uses
ShareMem,
SysUtils,
Classes,
XContact in 'XContact.pas',
UImpContact in 'UImpContact.pas';
{$R *.RES}
function ContactClass : TContactClass;
begin
result := TContact;
end;
function ContactListClass : TContactListClass;
begin
result := TContactList;
end;
exports
ContactClass, ContactListClass;
begin
end.
这就是DLL所需要做的工作。如果仔细检查代码,可以发现在DLL与其他应用程序之间惟一的不同之处在于,增加了抽象接口类的层次。现在看一下客户程序是如何实现的。
7.4.4 创建瘦客户程序
现在DLL程序已经完成(见上一节),客户程序就更简单了。为便于添加、删除、查找联系信息,创建了图7.8中的窗体。除了窗体上的组件之外,客户程序十分简单。
图7.8 用于测试联系信息和列表类的数据输入窗体示例
unit UTestContact;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs,
StdCtrls, XContact, ComCtrls;
type
TForm1 = class(TForm)
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
EditName: TEdit;
EditPhone: TEdit;
EditEMail: TEdit;
ButtonAdd: TButton;
ButtonRemove: TButton;
ButtonFind: TButton;
StatusBar1: TStatusBar;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure ButtonAddClick(Sender: TObject);
procedure ButtonRemoveClick(Sender: TObject);
procedure ButtonFindClick(Sender: TObject);
private
{ Private declarations }
FCurrentContact : IContact;
ContactList : IContactList;
Procedure UpdateCount( Count : Integer );
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
function ContactClass : TContactClass; external
'NewContactServer.dll';
function ContactListClass : TContactListClass; external
'NewContactServer.dll';
procedure TForm1.FormCreate(Sender: TObject);
begin
FCurrentContact := Nil;
ContactList := ContactListClass.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
ContactList.Free;
end;
procedure TForm1.ButtonAddClick(Sender: TObject);
begin
FCurrentContact := ContactClass.Create( EditName.Text,
EditPhone.Text, EditEMail.Text );
ContactList.Add( FCurrentContact );
UpdateCount( ContactList.Count );
end;
procedure TForm1.ButtonRemoveClick(Sender: TObject);
begin
if( Assigned(FCurrentContact)) then
begin
ContactList.Remove( FCurrentCOntact);
UpdateCount( ContactList.Count );
end;
end;
procedure TForm1.ButtonFindClick(Sender: TObject);
var
I : Integer;
begin
for I := 0 to ContactList.Count - 1 do
if( ContactList[I].Phone = EditPhone.Text ) then
begin
FCurrentContact := ContactList[I];
EditName.Text := FCurrentContact.Name;
EditPhone.Text := FCurrentContact.Phone;
EditEMail.Text := FCurrentContact.EMail;
exit;
end;
MessageDlg( 'Contact phone number not found', mtInformation, [mbOK], 0);
end;
Procedure TForm1.UpdateCount( Count : Integer );
begin
StatusBar1.SimpleText := Format( 'Count: %d', [Count] );
end;
end.
类中包含了构成界面的控件,以及五个事件方法。FormCreate和FormDestroy分别负责初始化和释放抽象类引用FCurrentContact和FContactList,而按钮单击事件方法则分别进行添加、删除和查找等行为。
在单元的实现部分,导入了DLL函数ContactClass和ContactListClass,从而提供了访问联系信息和联系信息列表类的途径(注意:接口部分的uses子句只能直接使用类的抽象版本)。FormCreate方法示范了如何通过ContactListClass类引用(它是一个TContactList类)来创建实际的联系信息列表(直接实例化IContactList类是错误的,虽然它是所声明的引用的类型)。导入的服务函数提供了对实现对象的访问,剩下的只要调用所需的函数即可。
这样工作就完成了。要跨越进程边界使用对象,首先要有抽象接口,它表示了要使用的功能,同时还要使DLL返回实现了该接口的子类化的版本。这也是个很好地描述了COM的工作方式的模型。现在DLL的复杂性已经解决了,您就可以处理技术方面的问题了。
7.5 小 结
第7章涵盖了一些支持瘦客户程序设计和DLL创建方面的话题。Delphi直接支持跨越模块边界传递对象。理解了接口和类引用,就可以很容易地编写出可重用的DLL,其中包含了所需的业务逻辑。本章对于理解组件对象模型(COM)是个很好的基础。在Delphi抽象接口和COM接口之间的相同之处要多于不同之处。利用本章中的技术,您可以编写出一些相似的Delphi应用程序和DLL,也可以作为进一步理解COM的踏脚石。