原型模式(Prototype)与Delphi对象克隆技术.doc
原型模式(Prototype)与Delphi对象克隆技术概述:在这篇文件中,讲述原型模式定义、结构、应用、实现。深入讨论了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);beginif Source <> nil then Source.AssignTo(Self) else AssignError(nil);end;由此可见, Assign方法实际上是调用AssignedTo方法来实现的,因此TPersistent的Assign方法很少被派生类所重载,但AssignTo却常被派生类根据需要重载。如果由AssignedTo方法来实现复制,那么必须保证源对象的类已经重写了AssignedTo方法,否则将抛出一个AssignError异常:procedure TPersistent.AssignError(Source: TPersistent);varSourceName: string;beginif 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; begina:= TMyObject.create; 关于对象a的代码. 开始克隆对象ab:= 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;interfaceuses 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;implementationTClient procedure TClient.opteration(NewObj:TPrototype);var NewObj:TPrototype;begin 克隆一个对象 NewObj:=FPrototype.Clone;end;TConcretePrototype1 function TConcretePrototype1.Clone: TObject;begin克隆的实现代码end;TConcretePrototype2function 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;interfaceuses 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;implementationTClient 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;TConcretePrototype2function TConcretePrototype2.Clone: TObject;begin/具体实现代码end;TPrototypeManagerconstructor 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.ItemsIndex);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;interfaceuses 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; implementationTPrototype_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;interfaceuses 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 *.dfmprocedure 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对象的深克隆前面我讲过,当克隆一个对象时,如果克隆的是该对象所有的状态,即被克隆对象的所有变量都含