关键字: 原型模式,对象克隆,Prototype,Clone
概述:
在这篇文件中,讲述原型模式定义、结构、应用、实现。深入讨论了Delphi中原型模式的实现方法。特别研究了原型模式的核心技术——对象克隆技术在Delphi中的实现途径。本文还探讨了对象引用复制和对象克隆的区别,以及VCL的持久对象机制、对象流化技术。
1、原型模式解说
原型模式通过给出一个原型对象来指明所要创建对象的类型,然后克隆该原型对象以便创建出更多同类型的新对象。
例如:在Delphi的IDE中,我们为设计窗体拖放了一个按钮对象。为了快速创建更多的同样字体和尺寸的按钮对象,我们可以复制该按钮(使用菜单Copy菜单或快捷键Ctrl+C),并在设计窗体多次粘贴(使用菜单Paste菜单或快捷键Ctrl+V。
设计窗体中的按钮对象是用于我们应用程序的,而IDE中提供的按钮对象创建方法(复制和粘贴)则是属于Delphi架构的。我们通过复制创建一个按钮对象时,不需要知道Delphi是如何实现的。
所 要说明的是,虽然我们使用的是类似文字处理中的复制和粘贴,但复制的决不是一个按钮对象的外观(字体和尺寸等),而是整个按钮对象,包括它的属性和方法。 所以,更严格讲,我们是克隆了这个对象,即得到一个和源对象一样的新对象。我们称这种被克隆的对象(比如按钮)为原型。只要系统支持克隆功能,我们就可以 任意克隆对象。
由此可见,原型模式适用于系统应该与其对象的创建、组合及显示时无关的情况,包括:
-当要实例化的类是在运行时刻指定时,例如,通过动态载入。
-当类实例只是少数不同组合状态其中之一时,这时比较好的方式在适当的状态下使用一些组合的原型并复制他们,而不是人工的继承这些类。
-避免建立工厂类等级结构平行产出类等级结构时。
假 设一个系统的产品类是动态加载的,而且产品类具有一定的等极结构。这个时候如果采取工厂模式的话,工厂类就不得不具有一个相应的等级。而产品类的等级结构 一旦变化,工厂类的等级结构就不得不有一个相应的变化。这对于产品结构可能会有经常性变化的系统来说,采用工厂模式就有不方便之处。
这时如果采取原型模式,给每一个产品类配备一个克隆方法(大多数的时候只需给产品类等级结构的基类配备一个克隆方法),便可以避免使用工厂模式所带来的具有固定等级结构的工厂类。
这样,一个使用了原型模式的系统与它的产品对象是如何创建出来的,以及这些产品对象之间的结构是怎样的,还有这个结构会不会发生变化,都是没有关系的。
2、Delphi对象的克隆
原型模式通过克隆原型对象来创建新对象,因此了解和掌握Delphi中对象的克隆是使用原型模式的关键。
在Delphi创建一个对象实际上就是把一个类进行实例化。例如要从TMan类创建一个名为Tom的对象,可以这样创建:
var Tom:TMan;
......
Tom:=TMan.Create;
以上语句完成了以下工作:
-声明TMan类型的变量Tom;
-为TMan类创建一个实例;
-将变量Tom指向创建的实例。
我们从中可以发现,对象变量和对象并不是一回事。对象是TMan类创建的一个实例,对象变量是该对象的引用。为了简单,在称呼上我们通常并不严格区分。但在使用时,务必分清对象引用和实际对象。
有时在使用对象时无需使用对象变量来区分某一对象,例如:
Factory.MakeTool(TMan.Create);
这里无需区分TMan的实例是Tom还是Jack。
但我们使用以下例子时,表示Tom和Jack分别引用了不同的TMan的实例,此时他们是两个对象。
var Tom,Jack:TMan;
......
Tom:=TMan.Create;
Jack:=TMan.Create;
但是如果接着使用以下语句:
Tom:=Jack;
此时Tom变量就不再引用Tom对象,而是引用Jack对象,这就好像Tom变成了Jack的另一个名字。当你找Tom时,找到的是Jack。所以这种方法只能复制对象的引用而不能克隆整个对象。
由 此我们了解到,对象是类的动态实例,对象总是被动态分配到堆上。因此一个对象引用就如同一个句柄或一个指针。但你分配一个对象引用给一个变量 时,Delphi仅复制引用,而不是整个对象。在Delphi中使用一个对象的唯一方法就是使用对象引用。一个对象引用通常以一个变量的形式存在,但是也 有函数或者属性返回值的形式。
Delphi中不像有的语言那样提供了对象克隆的功能(比如:Java有Object.clone方法),所以在Delphi中实现对象克隆的功能需要自己编写代码。
好在VCL的体系结构中,TPersistent类系下的对象可以通过覆盖Assign方法,实现克隆行为。TPersistent的Assign方法较常用于两个对象属性的复制。在Assign方法中可以完成对象属性、方法和事件的逐个复制。
Assign方法在TPersistent类中声明为虚方法,以便允许每个派生类定义自己的复制对象方法。如果派生类没有重写Assign方法,则TPersistent的Assign方法会将复制动作交给源对象来进行:
procedure TPersistent.Assign(Source: TPersistent);
begin
if Source <> nil then Source.AssignTo(Self)
else AssignError(nil);
end;
由此可见, Assign方法实际上是调用AssignedTo方法来实现的,因此TPersistent的Assign方法很少被派生类所重载,但AssignTo却常被派生类根据需要重载。
如果由AssignedTo方法来实现复制,那么必须保证源对象的类已经重写了AssignedTo方法,否则将抛出一个AssignError异常:
procedure TPersistent.AssignError(Source: TPersistent);
var
SourceName: string;
begin
if Source <> nil then
SourceName := Source.ClassName else
SourceName := 'nil';
raise EConvertError.CreateResFmt(@SAssignError, [SourceName, ClassName]);
end;
那么在程序中是如何使用Assign方法实现对象克隆的呢?如果要让对象b克隆对象a,则可以考虑以下赋值操作:
......
var
{
假设这里的TMyObject是TPersistent的派生类,
并实现了Assign方法。比如:TFont 。
}
a,b:TMyObject;
begin
a:= TMyObject.create;
{ 关于对象a的代码}
......
{ 开始克隆对象a}
b:= TMyObject.create;
b.Assign(a);//对象b的属性和内容和对象a完全相同。
end;
由此可见,b:=a意味着b是a的引用,即两者是同一对象。如果写成b.Assign(a),那么b是仍然一个独立的对象,其状态与a相同,也就可以看成是b克隆了a。
3、结构和用法
原始模式的结构如图所示。它涉及到三个参与者:
-抽象原型(Prototype)——声明一个接口以便克隆其本身。
-具体原型(ConcretePrototype)——实现克隆的操作以克隆本身。
-客户(Client)——请求原型克隆其本身以构建新的对象。
他们之间的合作关系为客户请求原型克隆复制其本身以构建新的对象。
原型模式的实现代码模板如示例程序 9 1所示。
示例程序 9 1原型模式的实现代码模板
unit Prototype;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs;
type
TPrototype = class (TObject)
public
function Clone: TObject; virtual; abstract;
end;
TConcretePrototype1 = class (TPrototype)
public
function Clone: TObject; override;
end;
TConcretePrototype2 = class (TPrototype)
public
function Clone: TObject; override;
end;
TClient = class (TObject)
private
FPrototype: TPrototype;
public
procedure opteration;
end;
implementation
{TClient }
procedure TClient.opteration(NewObj:TPrototype);
var
NewObj:TPrototype;
begin
{克隆一个对象}
NewObj:=FPrototype.Clone;
end;
{TConcretePrototype1
function TConcretePrototype1.Clone: TObject;
begin
{克隆的实现代码}
end;
{TConcretePrototype2}
function TConcretePrototype2.Clone: TObject;
begin
{克隆的实现代码}
end;
end.
前 面我介绍的原型模式通常用于原型对象数目较少且比较固定的情况,这种情况下原型对象的引用由客户对象自己保存。但是,如果要创建的原型对象数目不是很固 定,则可以采用下面介绍的注册形式的原型模式。在这种情况下,客户对象不用保存对原型对象的引用,而是另有原型管理器负责。
我们把负责注册原型对 象的参与者称为原型管理器(prototype manager)。原型管理者储存原型并依据一定的键值(或索引值)传回相对应的原型,它包含了原型对象的添加、删除等操作,并使每个原型对象相对应一个 键值(或索引值)。客户端可以在运行期改变或者浏览这个注册项。这样一来,客户端在系统中管理及扩充原型对象数量都无须撰写更多的程序代码。Delphi 的TObjectList类可以帮我们实现原型对象的注册和管理。
注册形式的原型模式结构图比前图 多了一个原型管理器(TPrototypeManager)。原型管理器负责创建具体原型类的对象,并记录每一个被创建的对象。
示 例程序 9 2给出了一个示意性实现的源代码。首先,抽象原型TPrototype声明了一个Clone方法,然后由具体原型TConcretePrototype1 和TConcretePrototype2分别实现该Clone方法。管理器TPrototypeManager为注册原型对象提供必要的方法,包括:新 增、获取、计数等。它实际上是通过Delphi的TObjectList自动实现这些功能的。
系统中的客户对象通常先创建一个新的原型对象,然后 克隆一份,注册并保存在原型管理器中。在注册形式的原型模式中,当客户对象克隆一个原型对象之前,客户对象先查看管理对象中是否已经存在有满足要求的原型 对象。如果有就直接从管理对象中取得该对象的引用;如果没有,则克隆并注册该原型对象。
------------------------
示例程序 9 2注册形式的原型模式实现代码模板
unit Prototype2;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs,Contnrs;
type
TPrototype = class (TObject)
public
function Clone: TObject; virtual; abstract;
end;
TConcretePrototype1 = class (TPrototype)
public
function Clone: TObject; override;
end;
TConcretePrototype2 = class (TPrototype)
public
function Clone: TObject; override;
end;
TPrototypeManager = class (TObject)
private
FObjects: TObjectList;
public
constructor create; override;
procedure add(AObject: TPrototype);
function Count: Integer;
function get(Index: Integer): TPrototype;
end;
TClient = class (TObject)
private
FPrototype: TPrototype;
FPrototypeManager: TPrototypeManager;
public
constructor create; override;
procedure opteration;
procedure registerPrototype;
end;
implementation
{TClient }
procedure TClient.opteration;
begin
//具体实现代码
end;
constructor TClient.create;
begin
FPrototypeManager:=TPrototypeManager.Create;
end;
procedure TClient.registerPrototype;
var
ClonedObj:TPrototype;
i:integer;
begin
//示意性代码
FPrototype:=TConcretePrototype1.Create;
ClonedObj:=TPrototype(FPrototype.Clone);
FPrototypeManager.add(ClonedObj);
end;
{TConcretePrototype1 }
function TConcretePrototype1.Clone: TObject;
begin
//具体实现代码
end;
{TConcretePrototype2}
function TConcretePrototype2.Clone: TObject;
begin
//具体实现代码
end;
{TPrototypeManager}
constructor TPrototypeManager.create;
begin
FObjects.Create;
end;
procedure TPrototypeManager.add(AObject: TPrototype);
begin
FObjects.Add(AObject);
end;
function TPrototypeManager.Count: Integer;
begin
result:=FObjects.Count;
end;
function TPrototypeManager.get(Index: Integer): TPrototype;
begin
result:=TPrototype(FObjects.Items[Index]);
end;
end.
4、原型模式范例详解
在许多程序中我们需要允许用户自己定义他们喜欢的字体。几乎所有Delphi的用户界面控件(UI)都提供TFont属性,用于设置字体。另外Delphi还提供字体对话框方便可视化操作。通常在需要允许用户设置字体的地方,程序员可以写上这样一段代码:
if FontDialog1.Execute then
Memo1.Font.Assign(FontDialog1.Font);
如 果在程序中有很多地方需要用户设置字体,这种代码就会出现在多处,并涉及到具体的用户界面控件,修改维护都很麻烦,而且用户也要费劲设置很多次字体。能不 能让用户只设置一次字体,然后复制到他们需要的各处?也就是说能不能先创建一个与使用字体界面无关的字体对象原型,然后在需要的地方克隆?我们不妨就用原 型模式来解决这个问题。
按照原型模式我设计的类图如图所示。演示界面TfrmClient是客户,它使用抽象类TPrototype_Font提 供的SetFont方法设置字体(原型对象)以及Clone函数克隆并返回克隆好的字体对象。但抽象类TPrototype_Font作为抽象原型仅仅是 一个接口,真正实现SetFont方法和Clone函数的是具体原型TPrototype_Font1和TPrototype_Font2。通过派 生,TPrototype_Font1类和TPrototype_Font2类可以提供不同的克隆产品和克隆实现方法。范例程序中我重点演示不同的克隆实 现方法。
示 例程序 9 3是原型字体程序的实现源码。在该程序中可以看到,我在TPrototype_Font1的Clone函数中调用了Assign方法来实现的字体克隆。 TFont实现了TPersistent的虚方法Assign,作为TFont派生类的TPrototype_Font1理所当然继承TFont的 Assign方法。
function TPrototype_Font1.Clone: TObject;
var
Temp: TPrototype_Font1;
begin
Temp:=TPrototype_Font1.Create;
Temp.Assign(self);
result:=Temp;
end;
如 果在某些类中没有Assign方法(或没有实现TPersistent的Assign方法),我们就不得不自己撰写Clone函数了。 TPrototype_Font2中的Clone函数就是自己编程实现克隆的示例。所谓克隆对象,实际上关键在复制该对象的状态,即对象的属性值(数据成 员变量的值)。因为有同一个类创建的对象之间的差异就在于这些属性值的不同。
function TPrototype_Font2.Clone: TObject;
var
Temp: TPrototype_Font2;
begin
Temp:=TPrototype_Font2.Create;
Temp.Name:=self.Name;
Temp.Size:=self.Size;
Temp.Color:=self.Color;
result:=Temp;
end;
但 是,有时候某些属性值并不是客户所需要的,所以在克隆时对对象的属性值进行取舍是允许的。TPrototype_Font2中的Clone函数中,我只复 制了影响字体外观的3个最主要的属性。这一取舍的后果是,克隆的新字体并不完全和原型字体一致,比如,不能反映出是否是斜体。这种克隆称为不完全克隆。
-------------
示例程序 9 4 原型字体程序的实现源码
unit PrototypeFont;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs;
type
TPrototype_Font = class (TFont)
public
function Clone: TObject; virtual; abstract;
procedure SetFont; virtual; abstract;
end;
TPrototype_Font1 = class (TPrototype_Font)
public
function Clone: TObject; override;
procedure SetFont; override;
end;
TPrototype_Font2 = class (TPrototype_Font)
public
function Clone: TObject; override;
procedure SetFont; override;
end;
implementation
{TPrototype_Font1调用Assign方法实现的字体克隆。}
function TPrototype_Font1.Clone: TObject;
var
Temp: TPrototype_Font1;
begin
Temp:=TPrototype_Font1.Create;
Temp.Assign(self);
result:=Temp;
end;
procedure TPrototype_Font1.SetFont;
var
FontDialog: TFontDialog;
begin
FontDialog:= TFontDialog.Create(nil);
try
if FontDialog.Execute then
TFont(self).Assign(FontDialog.Font);
finally
FontDialog.Free;
end;
end;
{TPrototype_Font2 通过自己编程实现的字体克隆。}
function TPrototype_Font2.Clone: TObject;
var
Temp: TPrototype_Font2;
begin
Temp:=TPrototype_Font2.Create;
Temp.Name:=self.Name;
Temp.Size:=self.Size;
Temp.Color:=self.Color;
result:=Temp;
end;
procedure TPrototype_Font2.SetFont;
var
FontDialog: TFontDialog;
begin
FontDialog:= TFontDialog.Create(nil);
try
if FontDialog.Execute then
begin
self.Name:=FontDialog.Font.Name;
self.Size:=FontDialog.Font.Size;
self.Color:=FontDialog.Font.Color;
end;
finally
FontDialog.Free;
end;
end;
end.
在示例程序 9 4所示的客户程序中,我设计了一个演示界面。通过“设置字体”按钮可以设置原型字体,然后点击“<-克隆”按钮,可以将原型字体克隆到左边的文本编辑框中。如图所示。
注 意这里使用的多态和转型技术。TPrototype_Font的clone函数把克隆的对象向下转型为TObject传出,示例程序 9 4中再把返回的对象向上转型为TPrototype_Font。因为TFont是TPrototype_Font的基类,下面的写法是安全的。
Memo1.Font:=TPrototype_Font(FPrototype_Font1.clone);
示例程序 9 5 客户程序源码
unit MainForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls,PrototypeFont, ExtCtrls;
type
TForm1 = class (TForm)
Memo1: TMemo;
btnSet1: TButton;
btnClone1: TButton;
btnClone2: TButton;
Memo2: TMemo;
btnSet2: TButton;
Bevel1: TBevel;
procedure FormCreate(Sender: TObject);
procedure btnClone1Click(Sender: TObject);
procedure btnSet1Click(Sender: TObject);
procedure btnClone2Click(Sender: TObject);
procedure btnSet2Click(Sender: TObject);
private
FPrototype_Font1: TPrototype_Font;
FPrototype_Font2: TPrototype_Font;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
FPrototype_Font1:=TPrototype_Font1.Create;
FPrototype_Font2:=TPrototype_Font2.Create;
Memo1.Lines.Add(
'这里演示调用Assign方法实现的字体克隆。') ;
Memo2.Lines.Add(
'这里演示通过自己编程实现的字体克隆。') ;
end;
procedure TForm1.btnClone1Click(Sender: TObject);
begin
Memo1.Font:=TPrototype_Font(FPrototype_Font1.clone);
end;
procedure TForm1.btnSet1Click(Sender: TObject);
begin
FPrototype_Font1.SetFont;
end;
procedure TForm1.btnClone2Click(Sender: TObject);
var
Prototype_Font2: TPrototype_Font2;
begin
Memo2.Font:=TPrototype_Font(FPrototype_Font2.clone);
end;
procedure TForm1.btnSet2Click(Sender: TObject);
begin
FPrototype_Font2.SetFont;
end;
end.
5、使用Delphi流技术克隆对象实现原型模式
在 Delphi中,流对象与流化存储技术不仅是Delphi可视化设计实现的核心,它也为实现原型模式提供了更为方便、简单和通用的对象克隆方法。尽管流化 存储所涉及的存储媒介十分广泛,但在各对象的接口上得到了统一,使程序的存储操作变得十分方便、简单,从而使程序员能站在更高层面上进行对象存取的有关编 程工作而无需考虑存储介质的具体差异。
在VCL中。从TPersistent类开始提供了一个接口,引入了对象的可赋值性和流化 (assignment and streaming capabilities)等性质。TPersistent类是抽象类,没有实例对象。但该类实现了对象公布(Published)属性的存取,即在该类 及其派生类中声明为Published的属性、方法和事件等可在设计期时显示在Object Inspector窗中,能在Object Inspector中对对象的Published属性进行设计期的设计,并可将设置的值存到窗体或数据模块的DFM文件中。在程序运行期,对象将被初始化 为设计期所设置的状态。例如,在Delphi窗体上设计的按钮对象,在程序运行期该对象将被初始化为设计期所设置的状态。
在编程中为了实现流化, 我们需要使用TStream的派生类。TStream是所有流类的抽象基类,它继承自TObject。它的派生类主要用于对文本、内存、数据库的Blob 字段、数据压缩等进行操作。由于TStream与具体的存储无关,派生类却与存储媒介紧密相关,因此每个子类都必须实现与具体存储媒介相关的方法,如磁 盘、内存等。通过流的写方法我们可以把对象转化成位模式保存在内存或文件中;同样,通过流的读方法我们可以把保存在内存或文件中的位模式重新转化成对象。 通常把对象写到流里的过程称为串行化,而把对象从流中读出来的过程称为并行化。通过对象的一进一出,实际上就实现了对象的克隆。
需要注意的是,能够被流化的对象必须是TPersistent的派生类。一般Delphi的组件(TComponent的派生类)都是支持流化的。用户要使自己定义的类能够支持流化,通常至少应该使其继承自TPersistent。
————————————————————————————————————————
参阅:需要进一步了解Delphi对象流化机制以及VCL相关的类,请参阅我著的《Delphi面向对象编程思想》(机械工业出版社2003年9月)。
————————————————————————————————————————
下面我通过一个例子来演示说明如何使用流克隆对象实现原型模式。这个例子有点类似前图所示的通过Delphi的IDE复制和粘贴按钮对象。下图是演示程序的运行界面。我们点击“克隆对象”按钮,就会把一个文本框克隆到窗体上。
为此,我使用原型模式设计了下图的类结构。与前面原型模式示例程序不同的是,这里的TMemoPrototype类Clone方法利用了对象流化来完成对象的克隆。
function TMemoPrototype.Clone: TObject;
var
tmp:TMemoPrototype;
TmpStream:TStream;
begin
WriteComponentResFile('MemoPrototype.dat',self);
tmp:=TMemoPrototype(ReadComponentResFile('MemoPrototype.dat',nil));
result:=tmp;
end;
这 里我们使用了Delphi的Classes单元中两个全局函数WriteComponentResFile和ReadComponentResFile来 完成对象的串行化和并行化过程,前者将对象以位模式写到资源文件MemoPrototype.dat中,后者从资源文件 MemoPrototype.dat中读出对象,每读出一个对象就好像克隆出了一个对象。这两个函数封装了文件流(TFileStream)的操作过程, 使我们编程简单化,不过从他们的实现代码中(示例程序 9 6),我们还是可以看出这一流化过程的。
-------------
示例程序 9 6 WriteComponentResFile和ReadComponentResFile的实现代码
procedure WriteComponentResFile(const FileName: string;
Instance: TComponent);
var
Stream: TStream;
begin
Stream := TFileStream.Create(FileName, fmCreate);
try
Stream.WriteComponentRes(Instance.ClassName, Instance);
finally
Stream.Free;
end;
end;
function ReadComponentResFile(const FileName: string;
Instance: TComponent): TComponent;
var
Stream: TStream;
begin
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Result := Stream.ReadComponentRes(Instance);
finally
Stream.Free;
end;
end;
于是,在使用克隆对象的演示窗体中,我们通过点击“克隆对象”按钮,触发了以下按钮事件:
procedure TForm1.btnCloneClick(Sender: TObject);
var
tmp:TMemoPrototype;
begin
tmp:=TMemoPrototype(FPrototype1.clone);
tmp.Name:='Memo'+IntToStr(n);
tmp.Parent:=self;
tmp.Lines.Add('克隆文本框之'+IntToStr(n));
tmp.Left:=tmp.Left+30*n;
tmp.Top:=tmp.Top+30*n;
inc(n);
end;
通过克隆原型对象的方法,我们将方便地得到TMemoPrototype的实例。这些对象有着和原型对象一样的状态,比如:相同的外观和字体。(为了便于演示,我有意调整了他们的位置,否则窗体上的文本框会重叠在一起。)
由此可见,流化对象的强大之处在于无需复杂的处理就可以将对象和一个二进制流之间进行互相转化。这一功能可以巧妙地被我们用于对象克隆,特别是当某些对象不提供Assign方法实现时。
6、Delphi对象的深克隆
前 面我讲过,当克隆一个对象时,如果克隆的是该对象所有的状态,即被克隆对象的所有变量都含有与原始对象相同的值,称为完全克隆。否则,有选择地克隆原始对 象的部分状态,只能称为不完全克隆。这是从对象克隆的广度上看问题。如果从对象克隆的深度上看,对象克隆还可以分成浅克隆和深克隆。所谓浅克隆仅仅克隆所 考虑的对象,而不深入克隆它所引用的对象。如果在克隆当前对象时,同时也克隆了该对象所引用的对象,这就是所谓的深克隆了。在浅克隆中,被克隆的对象所引 用的对象仍然是原始对象所引用的那个对象;而深克隆中,被克隆的对象所引用的对象已经是一个新的对象。这就是说,深克隆同时也克隆了引用对象本身,而不是 它的一个引用。由于深克隆间接复制了引用的对象,如果出现循坏引用,可能会出现意想不到的问题。所以当一个类引用了不支持串行化的间接对象,或者引用含有 循环结构的时候,则不能考虑使用深克隆。
浅克隆和深克隆问题也称为浅复制和深复制(shallow copy versus deep copy)的问题,该问题的焦点在于是否复制实例变量,亦或只是共享该变量的引用。浅克隆是简单也容易达成的,但是复杂结构原型的克隆一般需要进行深复 制,因为这样可以保证原型对象与被克隆的对象彼此独立,互不影响。
虽然深克隆的问题比较复杂,但使用流化对象的技巧可以方便实现对象的深克隆。下面我给出一个演示性的例子。
下 图 是这个原型模式深克隆演示程序的类图。从图中看出,TStreamableClass包含了FContainedClass和FMemo两个数据成员变 量,分别引用了TContainedClass 和TMemoPrototype的实例对象。TStreamableClass的Clone方法实现了对象的深克隆。
示 例程序 9 7是PrototypeByStream单元程序的源码,这里实现了原型模式。需要注意的是,声明对象时,只有放置在published域的数据成员(或 属性)才能够被自动流化。也就是说,要克隆的引用对象变量要么直接放在published域(如TStreamableClass 的数据成员FMemo),要么将访问其的属性放在published域(如TStreamableClass 的属性ContainedClass)。
从流中读出对象时,需要将该对象转型为原来的类型,比如:
AClassInstance :=
TStreamableClass( ReadComponentResFile('DeepClone', nil));
但是事先要使用RegisterClass 或RegisterClasses注册自己的类,比如:
RegisterClasses([TMemoPrototype,TContainedClass, TStreamableClass]);
否则会找不到类,出现EClassNotFound异常。
示例程序 9 7 PrototypeByStream单元程序源码
unit PrototypeByStream;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs,StdCtrls;
type
TMemoPrototype = class (TMemo)
public
constructor create(AOwner:TComponent);override;
published
function Clone: TObject;
end;
TContainedClass = class(TPersistent)
private
FSomeData: Integer;
procedure SetSomeData(Value: Integer);
public
constructor Create;
published
property SomeData: Integer
read FSomeData write SetSomeData;
end;
TStreamableClass = class(TComponent)
private
FContainedClass: TContainedClass;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
FMemo:TMemoPrototype;
function Clone: TObject;
property ContainedClass: TContainedClass
read FContainedClass write FContainedClass;
end;
implementation
procedure TContainedClass.SetSomeData(Value: Integer);
begin
FSomeData := Value;
end;
constructor TContainedClass.Create;
begin
FSomeData := 42;
end;
constructor TStreamableClass.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FContainedClass := TContainedClass.Create;
FMemo:=TMemoPrototype.create(AOwner);
end;
destructor TStreamableClass.Destroy;
begin
FContainedClass.Free;
end;
function TStreamableClass.Clone: TObject;
var
AClassInstance: TStreamableClass;
begin
AClassInstance := TStreamableClass.Create(nil);
WriteComponentResFile('DeepClone', AClassInstance);
FreeAndNil(AClassInstance);
AClassInstance :=
TStreamableClass( ReadComponentResFile('DeepClone', nil));
result:=AClassInstance;
end;
{TMemoPrototype}
function TMemoPrototype.Clone: TObject;
var
tmp:TMemoPrototype;
TmpStream:TStream;
begin
WriteComponentResFile('MemoPrototype.dat',self);
tmp:=TMemoPrototype(ReadComponentResFile('MemoPrototype.dat',nil));
result:=tmp;
end;
constructor TMemoPrototype.create(AOwner:TComponent);
begin
inherited;
Width:=100;
Height:=50;
Left:=50;
Top:=50;
Font.Color:=clBlue;
end;
initialization
RegisterClasses([TMemoPrototype,TContainedClass, TStreamableClass]);
finalization
end.
在示例程序 9 8中,我使用并演示了使用流技术克隆对象的原型模式。除了在演示窗体上可以看到克隆的对象,我们还可以测试被克隆的引用对象是一个新对象还是原始对象的一个引用。
-----------------
示例程序 9 8 演示窗体的程序源码
unit PrototypeStreamForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls,PrototypeByStream;
type
TForm1 = class(TForm)
btnClone: TButton;
btnDeepClone: TButton;
procedure FormCreate(Sender: TObject);
procedure btnCloneClick(Sender: TObject);
procedure btnDeepCloneClick(Sender: TObject);
private
FPrototype1:TMemoPrototype;
FPrototype2:TStreamableClass;
n:integer;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
FPrototype1:=TMemoPrototype.create(self) ;
n:=1;
end;
procedure TForm1.btnCloneClick(Sender: TObject);
var
tmp:TMemoPrototype;
begin
tmp:=TMemoPrototype(FPrototype1.clone);
tmp.Name:='Memo'+IntToStr(n);
tmp.Parent:=self;
tmp.Lines.Add('克隆文本框之'+IntToStr(n));
tmp.Left:=tmp.Left+30*n;
tmp.Top:=tmp.Top+30*n;
inc(n);
end;
procedure TForm1.btnDeepCloneClick(Sender: TObject);
var
AInstance: TStreamableClass;
begin
FPrototype2:=TStreamableClass.create(self);
AInstance:=TStreamableClass(FPrototype2.Clone);
showmessage(inttostr(AInstance.ContainedClass.SomeData));
AInstance.ContainedClass.SomeData:=30;
showmessage(inttostr(AInstance.ContainedClass.SomeData));
AInstance.FMemo.Parent:=self;
AInstance.FMemo.Lines.Add('Deep Clone OK!');
//测试被克隆的引用对象是一个新对象还是原始对象的一个引用。
if (AInstance.FMemo=FPrototype2.FMemo) or
(AInstance.ContainedClass=FPrototype2.ContainedClass)
then
begin
showmessage('克隆的是对象引用!');
end;
end;
end.
这里我介绍的重点是模式及其应用实践。而Delphi中关于流的技术和对象流化机制还相当复杂,有兴趣的读者可以进一步查阅有关书籍和资料。