本帖介绍 Prototype Pattern (原型模式),并以一个「人事招聘程序」作为示例来说明。
--------------------------------------------------------
本帖的示例下载点:
http://files.cnblogs.com/WizardWu/090713.zip
第一个示例为 Console Mode (控制台应用程序) 项目,第二个示例为 ASP.NET 网站项目。
执行示例需要 Visual Studio 2008 或 IIS + .NET 3.0,不需要数据库。
--------------------------------------------------------
Prototype Pattern (原型模式)
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
- Design Patterns: Elements of Reusable Object-Oriented Software
原型模式就是以一个既有的原型实例当作范本,利用复制的方式,动态获得这个原型实例的状态以及全部的字段和属性,以此创建一或多个相同的对象,而且不需要知道任何创建的细节。
Prototype 模式打个通俗的比方:假如您在图书馆看到几本自己喜欢的书籍,当看到某些知识点时,想在上面作相关记号,但由于其是图书馆的书,不能在上面乱涂乱画。此时您只好把相关的章节,用复印机把它复印出来,然后在自己复印的纸张上作记号。在 Prototype Pattern 里,Clone 方法就如同此种复印的动作,用户从一个既有的原型实例 (如同图书馆里的书),复印后得到一或多个新的拷贝,不会破坏原本的原型,且用户不必知道原型的内容和格式。
Prototype 也具有一种「展示」的意味,就像是车展上的「原型」车款。当您对某个车款感兴趣时,您可购买相同的车款,而不是车展上展示的那辆车。
在软件设计方面,也常需要进行此种对象复制。例如我们要写一套室内设计软件,软件的操作界面上有一条 Toolbar,用户只要单击 Toolbar 上的 Button,或用鼠标拖曳到设计窗格中,就可创建一个桌子或椅子的副本,并可事后改变它的颜色或位置。当设计师改变设计图中的副本对象时,Toolbar 上的「原型」对象并不会跟着被改变。同样的观念,亦适用于工业设计 CAD 软件、图像处理软件,以及 Visual Studio 等各种软件的设计。
Prototype 模式的重点在于 Clone 方法,它负责复制 (克隆) 出一个新的对象并返回,而不是用 new 运算符和某个类的构造函数去创建实例。在此模式中,派生类如何覆写父类的 Clone 方法将是重点。而 clone 的方式,又可分为「浅拷贝 (shallow copy)」和「深拷贝 (deep copy)」,在介绍这个 Prototype 模式之前,先简单介绍一下这两种拷贝方式的差异 [1], [2], [3], [4] :
浅拷贝; 浅表复制 (shallow copy):对象拷贝时,如果字段是「值类型 (Value Type)」,则直接复制其值 (亦即复制整个字段);若字段为「引用类型 (Reference Type)」,则只复制其「引用 (reference; pointer)」,但不复制引用的字段,亦即若更改了任一个副本对象的某一个「引用类型」字段,则原型正本对象、其他副本对象,也全部会一并更改 (如同本帖的第三个示例 02_Employee / 02_ShallowCopy_fail.aspx.cs),也就是说正本和所有的副本,都指向了内存的同一个位置。深拷贝; 深层复制 (deep copy):不论对象的字段为「值类型」或「引用类型」,都会完整地复制,而且这些字段和属性都是完全独立的。在深拷贝中,所有的对象都是重复的。
另补充,.NET 的类型系统,分为「值类型」、「引用类型」两种,其对象在内存中的存储方式不同,如下:
值类型:只需要一段单独的内存,用于存储实际的数据在「栈 (Stack)」里,例如:int、byte、float、double、bool、struct、enum、char、...等类型。引用类型:需要两段内存,第一段存储实际的数据,其总是位于「堆 (Heap)」中;第二段是一个存在「栈」里的引用 (reference; pointer),其指向数据在「堆」中的实际存放位置,例如:object、string、class (包括自定义类)、interface、delegate、array (参考本帖的第三、第四个示例) 等类型。
但 string (字符串) 较特殊。string 虽然是「引用类型」,但却拥有「值类型」的特性。在 Prototype Pattern 及本帖的四个示例中,当透过 MemberwiseClone 方法做「浅拷贝」时,对象的 string 字段仍会被完整地复制,其结果就如同 int 等「值类型」的字段一样。
如下图 1 及下方示例 01_Shell,我们可透过自定义的 Prototype 抽象类,搭配 .NET 最顶层基类 System.Object 的 MemberwiseClone 方法,达成对象的「浅拷贝」,亦即复制某个对象其所有「字段 (field)」的值;但在 .NET 中,亦可舍弃此一自定义抽象类,让图 1 中的 ConcretePrototype1 类、ConcretePrototype2 类,改为实现 .NET 原生的 System.ICloneable 接口,透过实现此接口唯一的一个 Clone 方法,来达成对象的「浅拷贝」或「深拷贝」。
图 1 此图为 Prototype 模式的经典类图
01_Shell / Program.cs
using System;
namespace _01_Shell
{
//客户端程序
class Program
{
static void Main(string[] args)
{
ConcretePrototype1 p1 = new ConcretePrototype1("I"); //原型对象(来自外部的第一个实例)
ConcretePrototype1 c1 = (ConcretePrototype1)p1.Clone(); //浅拷贝(shallow copy)
Console.WriteLine("Cloned: {0}", c1.Id);
ConcretePrototype2 p2 = new ConcretePrototype2("II"); //原型对象(来自外部的第一个实例)
ConcretePrototype2 c2 = (ConcretePrototype2)p2.Clone(); //浅拷贝(shallow copy)
Console.WriteLine("Cloned: {0}", c2.Id);
Console.Read();
}
}
//每个具体的「原型」类,要实现的抽象类或接口。
//亦可舍弃此一自定义抽象类或接口,让 ConcretePrototype 类改为实现 .NET 原生的 System.ICloneable 接口
abstract class Prototype
{
private string id; //这个字段在派生类的 Clone 方法被调用时,会自动被拷贝
public Prototype(string id) //构造函数
{
this.id = id;
}
public string Id
{
get { return id; }
}
//返回 Prototype 类型。
public abstract Prototype Clone();
}
class ConcretePrototype1 : Prototype
{
public ConcretePrototype1(string id) //构造函数
: base(id)
{
}
//返回 Prototype 类型。在客户端程序中,依赖和获得的是 Prototype 抽象类,并以其定义来操作其派生类
public override Prototype Clone()
{
return (Prototype)this.MemberwiseClone(); //浅拷贝(shallow copy)
}
}
class ConcretePrototype2 : Prototype
{
public ConcretePrototype2(string id) //构造函数
: base(id)
{
}
//返回 Prototype 类型。在客户端程序中,依赖和获得的是 Prototype 抽象类,并以其定义来操作其派生类
public override Prototype Clone()
{
return (Prototype)this.MemberwiseClone(); //浅拷贝(shallow copy)
}
}
}
/*
执行结果:
Cloned: I
Cloned: II
*/
上方图 1 的 Class Diagram,以及「Shell (壳)」示例中,客户端程序透过抽象类 Prototype 的定义来操作其派生类。先选择一个「原型」实例,亦即 ConcretePrototype1 类或 ConcretePrototype2 类的实例,通过调用它所覆写抽象父类的 Clone 方法,获得一个和它一样、有相同 id 值的新对象,而非透过 new 运算符去创建实例。而拷贝完成后,拷贝的原型样本 (p1、p2),和副本 (c1、c2) 是两个独立的对象,可以独立变化和修改。
但为什么不用 new 运算符加上某个类的构造函数去创建实例,而要再衍生出这种 Prototype Pattern 呢?其中一个原因,是系统设计上,类的种类可能会很多,而难以整合成特定的自定义类时;或类与类之间有大量平行阶层结构,类的数量过多会造成管理上的困难。例如本帖一开始提到的室内设计软件,桌子类和椅子类,又可各分成: 方形的、圆形的、其他各种形状的…,若全部都要写成不同的类,类的数量会很可观。
此外,也是为了避免整个系统中,类与类之间结构的剧烈变化,避免为了重载一个新的函数,导致我们要修改的不是一个类,而是整个继承关系体系里的每一个类。
另一个原因,是用户在用鼠标操作这个室内设计软件时 (运行时期),若要重复创建相同的圆形桌子对象时,用 Prototype Pattern 这种对象复制的方式,由于对象初始化的内容都相同,会比从头用类去创建新的实例要容易,同时能在客户端程序中隐藏对象创建的细节,且在速度和性能上会较优 [15], [17]。
接下来的三个示例,为一个人事招聘系统的部分代码,我们以此为例来实现原型模式。某间公司要招聘「程序员」、「行政文员」两种职务,其中的 Employee 为顶层的抽象类,两个派生类 Developer 和 Typist 必须实现其 Clone 方法。示例执行结果如下图 3。
由于两个派生类 Developer 和 Typist,其成员都是 int 等「值类型」或 string,因此我们在客户端程序 (Page_Load) 中执行「浅拷贝」时,既有的第一个「原型」对象 - dev 和 typist 实例,其所有的字段,会被逐位复制到新对象 devCopy1、devCopy2、typistCopy1 中,且正本对象和副本对象的字段各自独立、存储在内存的不同位置。
此外,我们看到 Developer 第一个「拷贝」出来的对象 devCopy1,它的 Role、PreferredLanguage 字段,都和原型对象 dev 一样,是「资深工程师」和「C#」,而我们可以将 Name 字段改成「李大同」,而不影响原型对象 dev 的 Name 字段。
图 2 Sybase PowerDesigner 12.5 的「反向工程」功能,已可解析 C# 3.0 的 Auto-Implemented Property 语法
02_Employee / 01_ShallowCopy.aspx.cs
using System;
using com.cnblogs.WizardWu.Sample01;
//客户端程序
public partial class _01_ShallowCopy : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Developer dev = new Developer(); //原型对象(来自外部的第一个实例)
dev.Name = "王小明";
dev.Role = "资深工程师";
dev.PreferredLanguage = "C#";
Developer devCopy1 = (Developer)dev.Clone(); //浅拷贝(shallow copy)
devCopy1.Name = "李大同";
Developer devCopy2 = (Developer)dev.Clone(); //浅拷贝(shallow copy)
devCopy2.Name = "吴宇泽";
devCopy2.Role = "研发工程师";
devCopy2.PreferredLanguage = "C++";
Response.Write(dev + "
");
Response.Write(devCopy1 + "
");
Response.Write(devCopy2 + "
");
/* 执行结果:
王小明 - 资深工程师 - C#
李大同 - 资深工程师 - C#
吴宇泽 - 研发工程师 - C++
*/
Typist typist = new Typist(); //原型对象(来自外部的第一个实例)
typist.Name = "左婉青";
typist.Role = "行政文员";
typist.WordsPerMinute = 120;
Typist typistCopy1 = (Typist)typist.Clone(); //浅拷贝(shallow copy)
typistCopy1.Name = "周玉婷";
typistCopy1.WordsPerMinute = 115;
Response.Write(typist + "
");
Response.Write(typistCopy1 + "
");
/* 执行结果:
左婉青 - 行政文员 - 120 字/分
周玉婷 - 行政文员 - 115 字/分
*/
}
}
//服务器端程序
namespace com.cnblogs.WizardWu.Sample01
{
//员工 抽象类。
//每个具体的「原型」类,要实现的抽象类或接口
abstract class Employee
{
//返回 Employee 类型。
public abstract Employee Clone();
//.NET 3.0 的 Auto-Implemented Property (Automatic Properties) 语法,会自动产生对应的同名 private field
public string Name { get; set; } //姓名
public string Role { get; set; } //职务
}
//程序员
//继承 Employee 抽象类,或实现 .NET 的 ICloneable 接口,以便重写其 Clone 方法,创建作为当前实例副本的新对象
class Developer : Employee
{
public string PreferredLanguage { get; set; }
//返回 Employee 类型。在客户端程序中,依赖和获得的是 Prototype