CnPack Forum » 技术板块灌水区 » Delphi面向对象学习随笔八:物理封装


2008-6-24 14:04 bahamut8348
Delphi面向对象学习随笔八:物理封装

前面说过的封装其实是逻辑意义上的封装。逻辑封装是对某一特定逻辑功能模块的封装,这个特定逻辑功能块可以是一个类,当然也可以是一个包,他们都有自己的逻辑边界。另一种封装方式,我们通常叫它为物理封装:物理封装其实是具体实现代码的物理集合,他可以以bpl,dll,com+等形式体现。

    逻辑封装里,对象的传递、数据共享与调用相对要简单的多,只要我们引用类所定义的单元(unit)就可以直接访问类中public和published所公布出来的属性或方法,在编译的时候,编译器会把工程内所有引用的单元全部打包到exe中。逻辑封装最终是以一个独立的物理文件存在的。虽然简单,但是无法实现物理上的切割,一旦其中某个单元或代码段发生改动,那么其他的单元或代码段也需要重新编译和连接。

    而在物理封装中,对象的传递、数据共享与调用要复杂的多了,由于在编译的时候,exe和dll或bpl是两个或多个文件,所以你无法像在逻辑封装中那样简单的uses那个unit。而物理封装的好处是可以减少维护量,因为每个dll都是动态调用的,所以,我们只需要更新我们改动过的相应的dll,而其他的部分则可以不用改动。

  用DLL封装对象:
    用DLL封装函数,我想几乎是所有程序员熟悉到不能再熟悉的技术,而且我们可以找到很多相关的书籍和资料。这里我们只讨论怎么用DLL来封装对象。
    用DLL封装对象有以下的好处:
      一、可以节约内存。我们可以在使用到DLL资源的时候动态装载,不用时释放。
      二、提高代码重用。DLL在封装好以后,我们可以使用任何一个支持DLL的开发工具来调用它。
      三、可以使软件拆分成若干个小块,这样可以有效的降低维护量。
[color=red]    注意:如果你只为了减少软件体积而使用动态库,那么我建议你还是放弃使用动态库吧。[/color]

    当然,想使用DLL封装对象也有一定的困难:
      一、调用DLL的EXE只能使用DLL中对象的动态绑定的方法。
      二、DLL中的对象只能在DLL中创建。
      三、在DLL和调用方,都需要对封装的对象和被调用的方法进行声明。

    我们来看下面的例子:

    首先我们声明一个类:
[code]type
  TNewClass = class(TObject)
  public
    procedure SayHello; virtual;
      // 注意,这里不能使用静态方法,必须使用动态绑定(或者说晚绑定)技术。
      // 至于为什么——虚方法表有关,大家可以找其他资料详细研究^_^
  end;

procedure TNewClass.SayHello; // 实现部分
begin
  ShowMessage('Hello');
end;[/code]
    新建一个Library项目
[code]library dll;

function GetObj: TNewClass; stdcall;
begin // 创建对象
  Result:= TNewClass.Create;
end;

exports GetObj; // 定义输出函数
end.[/code]
    下面,我们创建一个EXE工程,并且添加类的声明:
[code]type
  TNewClass = class(TObject)
  public
    procedure SayHello; virtual; abstract;
      { 注意这里的声明方式和DLL中的不同,这里必须声明为virtual方法,还有由于此方法通过晚绑定用的是DLL中的实现,因此EXE中可不写其实现而声明成abstract方法。 }
  end;

function GetObj: TNewClass; stdcall; external 'dll.dll';[/code]
    之后,我们可以添加一段测试代码来测试我们是否实现了DLL对象的共用:
[code]var
  NewClass: TNewClass;
begin
  NewClass:= GetObj;
  if not Assigned(NewClass) then
    Exit;

  try
    NewClass.SayHello;
  finally
    FreeAndNil(NewClass);
  end;
end;[/code]
    我们可以看到,这的确达到了EXE与DLL之间传递对象的目的。
    但是,有点麻烦:首先,在DLL工程与EXE工程都需要有被封装对象的定义。其次,virtual和abstract必须正确使用。还有,如果一旦对象发生变化,那么两边的定义都需要修改,这样难免会出点小错。

    其实,我们可以使用接口来进行对象的传递,上面的例子我们可以稍微修改一下:

    首先,我们定义一个接口:
[code]type
  INewInterface = interface(IInterface)
    procedure SayHello(); // 定义我们要的方法
  end;[/code]
    另外,修改TNewClass类的声名:
[code]type
  TNewClass = class(TInterfacedObject, INewInterface)
  public
    procedure SayHello();
  end;[/code]
    实现部分无须改动。
    接着,我们修改先前的那个Library项目:
[code]library dll;

function GetObj: INewInterface; stdcall;
begin // 创建对象
  Result:= TNewClass.Create;
end;

exports GetObj; // 定义输出函数
end.[/code]
    在EXE工程中,我们直接引用接口定义的单元,并且修改输出函数的声明:
[code]function GetObj: INewInterface; stdcall; external 'dll.dll';[/code]
    之后,测试代码会成这样:
[code]var
  NewInterface: INewInterface;
begin
  NewInterface:= GetObj;
  NewInterface.SayHello;
  NewInterface:= nil;
end;[/code]
    这样做的好处是,我们可以避免在多处重复说明一个要传递的对象的声明,只要我们需要的方法的声明方式不动,我们只需要改动TNewClass的实现代码,而无需改动EXE程序中的任何代码部分。
    PS:Delphi的OpenToolsAPI接口就是这个通过接口共享对象原理的很典型的应用。(这是刘啸说的。老实说,这个用法是我在写这个笔记的时候临时想到的,因为从来没有使用过未经COM封装的interface。哪里知道竟然和OpenToolAPI一样的原理,自己YY下^_^)

    当然我们还可以使用COM来封装对象:
    首先,我们建立一个名为NewCom的COM模型,建立COM模型前一篇已经说过,这里不再重复。
    那么,我们的调用代码就会变成这样:
[code]var
  NewCom: ITNewCom;
begin
  NewCom:= CoTNewCom.Create;
    // 当然,和我前一篇一样使用CreateComObject函数也是一样的
  NewCom.SayHello;
  NewCom:= nil;
end;[/code]
    我们可以看到,实现代码几乎没什么改动。那么,假如我们什么时候要把SayHello的实现代码:
ShowMessage('Hello');
    改成:
MessageBox(0, 'Hello', 'SayHello', MB_OK);
    那么,我们只需要更新这个COM文件,调用它的EXE程序无须改动,这就是接口的优点。

[[i] 本帖最后由 bahamut8348 于 2008-6-24 21:48 编辑 [/i]]

2008-6-25 09:26 Passion
关键是BPL封装后它的公开的类以及方法接口属性等都能通过外部在源码中直接访问,也就没了什么技术上封装的说法了。BPL封装更多的是体现在其逻辑上。

2008-6-25 16:30 bahamut8348
刘艺先生的书我当然看过,但是一般我看书只是会着重的看理论部分,关于例子看的很少,一般都是PASS掉的,呵。毕竟我们看书是为了学习别人的思想而不是为了抄别人的例子

2008-7-1 21:21 lixupeng
顶不过有点深噢

页: [1]


Powered by Discuz! Archiver 5.0.0  © 2001-2006 Comsenz Inc.