原型模式

思考并回答以下问题:

本章导学

原型模式是一种特殊的创建型模式,它通过复制一个已有对象来获取更多相同或者相似的对象。原型模式可以提高相同类型对象的创建效率,简化创建过程。

本章将学习原型模式的工作原理和结构,学习如何通过C#语言来实现原型模式,理解浅克隆和深克隆两种机制的异同,并通过实例学习如何实现浅克隆和深克隆。

本章知识点

  • 原型模式的定义
  • 原型模式的结构
  • 浅克隆与深克隆
  • 原型模式的实现
  • 原型模式的应用
  • 原型模式的优缺点
  • 原型模式的适用环境
  • 原型管理器

原型模式概述

【西游记】中“孙悟空拔毛变小猴”的故事几乎人人皆知,孙悟空可以用毫毛根据自己的形象,复制出很多和自己长得一模一样的“身外身”来,如图1所示。

图1 孙悟空拔毛变小猴

孙悟空这种根据自己的形象复制(克隆)出多个身外身的技巧,在面向对象软件设计领域被称为原型模式,孙悟空则被称为原型对象。在面向对象系统中,也可以通过复制一个原型对象得到多个与原型对象一模一样的新对象,这就是原型模式的动机。

原型模式的定义如下:

1
使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。

原型模式是一种对象创建型模式,它的工作原理很简单:将一个原型对象传给要发动创建的对象(即客户端对象),这个要发动创建的对象通过请求原型对象复制自己来实现创建过程。由于在软件系统中经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在软件开发中具有较高的使用频率。原型模式是一种“另类”的创建型模式,创建新对象(也称为克隆对象)的工厂就是原型类自身,工厂方法由负责复制原型对象的克隆方法来实现。

需要注意的是,通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常,对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式对克隆对象进行修改以后,可以得到一系列相似但不完全相同的对象。

原型模式的结构与实现

原型模式的结构

原型模式的结构如图2所示。

图2 原型模式结构图

由图2可知,原型模式包含以下3个角色。

(1)Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口,甚至可以是具体实现类。

(2)ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。

(3)Client(客户类):在客户类中,让一个原型对象克隆自身从而创建一个新的对象,只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此,用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。

浅克隆与深克隆

根据在复制原型对象的同时是否复制包含在原型对象中引用类型的成员变量,原型模式的克隆机制分为两种:浅克隆(Shallow Clone)和深克隆(Deep Clone)。

1.浅克隆

在浅克隆中,如果原型对象的成员变量是值类型(如int、double、byte、bool、char等基本数据类型),将复制一份给克隆对象;如果原型对象的成员变量是引用类型(如类、接口、数组等复杂数据类型),则将引用对象的地址复制一份给克隆对象,也就是说,原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当原型对象被复制时,只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制,如图3所示。

图3 浅克隆示意图

2.深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将被复制,如图4所示。

图4 深克隆示意图

原型模式的实现

实现原型模式的关键在于如何实现克隆方法,下面介绍两种在C#语言中常用的克隆实现方法。

1.通用的克隆实现方法

通用的克隆实现方法是在具体原型类的克隆方法中创建一个与自身类型相同的对象并将其返回,是创建的同时将相关参数传入新创建的对象中,保证它们的成员变量相同。示意代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
abstract class Prototype
{
public abstract Prototype Clone();
}

class ConcretePrototype : Prototype
{
private string attr; //成员变量

public string Attr
{
get { return attr; }
set { attr = value; }
}

// 克隆方法
public override Prototype Clone()
{
ConcretePrototype prototype = new ConcretePrototype();
prototype.Attr = this.Attr;
return prototype;
}
}

在客户类中只需要创建一个ConcretePrototype对象作为原型对象,然后调用其Clone()方法即可得到对应的克隆对象,如下面的代码片段所示:

1
2
3
4
...
ConcretePrototype prototype = new ConcretePrototype();
ConcretePrototype copy = (ConcretePrototype)prototype.Clone();
...

此方法是原型模式的通用实现方法,与编程语言本身的特性无关,除C#外,其他面向对象编程语言也可以使用这种形式来实现对原型的克隆。

在原型模式的通用实现方法中,可通过手工编写Clone()方法来实现浅克隆和深克隆。对于引用类型的对象,可以在Clone()方法中通过赋值的方式来实现复制,这是一种浅克隆实现方案;如果在Clone()方法中通过创建一个全新的成员对象来实现复制,则是一种深克隆实现方案。C#语言中的字符串(string/String)对象存在特殊性,只要两个字符串的内容相同,无论是直接赋值还是创建新对象,它们在内存中始终只有一份。

2.C#中的MemberwiseClone()方法和ICloneable接口

在C#语言中,提供了一个MemberwiseClone()方法用于实现浅克隆,该方法使用起来很方便,直接调用一个已有对象的MemberwiseClone()方法即可实现克隆。如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 成员类
class Member
{

}

class ConcretePrototypeA
{
private Member member; // 成员对象

public Member Member
{
get { return member; }
set { member = value; }
}

// 克隆方法
public ConcretePrototypeA Clone()
{
return (ConcretePrototypeA)this.MemberwiseClone(); // 浅克隆
}
}

在客户类中可以直接调用原型对象的Clone()方法来创建新的对象,如下面的代码片段所示:

1
2
3
4
5
6
7
...
ConcretePrototypeA prototype, copy;
prototype = new ConcretePrototypeA();
copy = prototype.Clone();
Console.WriteLine(prototype == copy);
Console.WriteLine(prototype.Member == copy.Member);
...

在上述客户类代码片段中,输出语句“Console.WriteLine(prototype==copy);”的输出结果为“False”,输出语句“Console.WriteLine(prototype.Member==copy.Member);”的输出结果为“True”,表明此处的克隆方法为浅克隆。

除了MemberwiseClone()方法以外,在C#语言中还提供了一个ICloneable接口,它也可以用来创建当前对象的副本,其代码如下:

1
2
3
4
public interface ICloneable
{
object Clone();
}

ICloneable接口充当了抽象原型类的角色,具体原型类通常作为实现该接口的子类,如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ConcretePrototypeB : ICloneable // 实现ICloneable接口
{
private Member member;

public Member Member
{
get { return member; }
set { member = value; }
}

// 实现深克隆
public object Clone()
{
ConcretePrototypeB copy = (ConcretePrototypeB)this.MemberwiseClone();
Member newMember = new Member();
copy.Member = newMember;
return copy;
}
}

客户端代码片段如下:

1
2
3
4
5
6
7
...
ConcretePrototypeB prototype, copy;
prototype = new ConcretePrototypeB();
copy = (ConcretePrototypeB)prototype.Clone();
Console.WriteLine(prototype == copy);
Console.WriteLine(prototype.Member == copy.Member);
...

在此客户类代码片段中,输出语句“Console.WriteLine(prototype==copy);”的输出结果为“False”,输出语句“Console.WriteLine(prototype.Member==copy.Member);”的输出结果也为“False”,表明此处的克隆方法为深克隆。

在实现ICloneable接口时,通常提供的是除MemberwiseClone()以外的深克隆方法。除了通过直接创建新的成员对象来手工实现深克隆外,还可以通过反射、序列化等方式来实现深克隆,在使用序列化实现时要求所有被引用的对象都必须是可序列化的(Serializable)。在下一节的应用实例中将学习如何使用序列化来实现深克隆。

原型模式的应用实例

下面通过一个应用实例来进一步学习和理解原型模式。

1.实例说明

在使用某OA系统时,有些岗位的员工发现他们每周的工作都大同小异,因此在填写工作周报时很多内容都是重复的,为了提高工作周报的创建效率,大家迫切地希望有一种机制能够快速创建相同或者相似的周报,包括创建周报的附件。试使用原型模式对该OA系统中的工作周报创建模块进行改进。

2.实例类图

通过分析,本实例的结构如图5所示。

图5 工作周报创建模块结构图

在图5中,WeeklyLog充当原型角色,Clone()方法为克隆方法,用于实现原型对象的克隆,Attachment充当成员类。

3.实例代码

(1)WeeklyLog:周报类,充当原型角色。在真实环境下该类比较复杂,考虑到代码的可读性,在此只列出部分与模式相关的核心代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System;
using System.IO;
using System.Collections;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;

namespace PrototypeSample
{
[Serializable]
class WeeklyLog
{
private Attachment attachment;
private string name;
private string date;
private string content;

public Attachment Attachment
{
get { return attachment; }
set { attachment = value; }
}

public string Name
{
get { return name; }
set { name = value; }
}

public string Date
{
get { return date; }
set { date = value; }
}

public string Content
{
get { return content; }
set { content = value; }
}

/*
//使用MemberwiseClone()方法实现浅克隆
public WeeklyLog Clone()
{
return this.MemberwiseClone() as WeeklyLog;
}
*/

//使用序列化方式实现深克隆
public WeeklyLog Clone()
{
WeeklyLog clone = null;
FileStream fs = new FileStream("Temp.dat", FileMode.Create);
BinaryFormatter formatter = new BinaryFormatter();
try
{
formatter.Serialize(fs, this);
}
catch (SerializationException e)
{
Console.WriteLine("Failed to serialize. Reason: " + e.Message);
throw;
}
finally
{
fs.Close();
}

FileStream fs1 = new FileStream("Temp.dat", FileMode.Open);
BinaryFormatter formatter1 = new BinaryFormatter();
try
{
clone = (WeeklyLog)formatter1.Deserialize(fs1);
}
catch (SerializationException e)
{
Console.WriteLine("Failed to deserialize. Reason: " + e.Message);
throw;
}
finally
{
fs.Close();
}
return clone;
}
}
}

(2)Attachment:附件类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;

namespace PrototypeSample
{
[Serializable]
class Attachment
{
private string name;

public string Name
{
get { return name; }
set { name = value; }
}

public void Download()
{
Console.WriteLine("下载附件,文件名为{0}。",name);
}
}
}

(3)Program:客户端测试类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;

namespace PrototypeSample
{
class Program
{
static void Main(string[] args)
{
WeeklyLog log_previous, log_new;
log_previous = new WeeklyLog();
Attachment attachment = new Attachment();
log_previous.Attachment = attachment;
log_new = log_previous.Clone();
Console.WriteLine("周报是否相同?{0}",(log_previous == log_new)?"是":"否");
Console.WriteLine("附件是否相同?{0}",(log_previous.Attachment == log_new.Attachment)?"是":"否");
Console.Read();
}
}
}

4.结果及分析

编译并运行程序,输出结果如下:

1
2
周报是否相同?否
附件是否相同?是

从输出结果可以得知,在本实例中周报对象被成功复制,但是附件对象并没有被复制,实现了浅克隆。

5.深克隆解决方案

为了能够在复制周报的同时复制附件对象,需要采用深克隆机制。本节将介绍如何通过序列化的方式来实现深克隆,使用序列化实现深克隆包含两个步骤。

首先必须将周报类WeeklyLog和附件类Attachment标记为可序列化(Serializable),如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
[Serializable]
class WeeklyLog
{
private Attachment attachment;
...
}

[Serializable]
class Attachment
{
...
}

然后将周报类WeeklyLog的Clone()方法修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 使用序列化方式实现深克隆
public WeeklyLog Clone()
{
WeeklyLog clone = null;

FileStream fs = new FileStream("Temp.dat", FileMode.Create);
BinaryFormatter formatter = new BinaryFormatter();

try
{
formatter.Serialize(fs, this); // 序列化
}catch(SerializationException e)
{
Console.WriteLine("Failed to serialize. Reason: " + e.Message);
throw;
}finally
{
fs.Close();
}

FileStream fs1 = new FileStream("Temp.dat", FileMode.Open);
BinaryFormatter formatter1 = new BinaryFormatter();

try
{
clone = (WeeklyLog)formatter1.Deserialize(fs1); // 反序列化
}catch(SerializationException e)
{
Console.WriteLine("Failed to serialize. Reason: " + e.Message);
throw;
}finally
{
fs1.Close();
}

return clone;
}

重新执行客户端类Program,输出结果如下:

1
2
周报是否相同?否
附件是否相同?否

从输出结果可以得知,本实例中,在成功复制周报对象的同时附件对象也被复制,实现了深克隆。

在上述深克隆实现代码中,通过使用FileStream类和BinaryFormatter类可实现对象的序列化和反序列化操作,首先使用序列化将当前对象写入流中,然后使用反序列化从流中获取对象。由于在序列化时一个对象的成员对象将伴随该对象一起被写入流中,在反序列化时将得到一个包含成员对象的新对象,因此可采用序列化和反序列化联用来实现深克隆。

原型管理器

原型管理器(Prototype Manager)将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。在原型管理器中针对抽象原型类进行编程,以便于扩展,其结构如图6所示。

图6 带原型管理器的原型模式

图6中典型的原型管理器PrototypeManager类的实现代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System.Collections;

namespace PrototypeManagerSample
{
abstract class Prototype
{
public abstract Prototype Clone();
}

class ConcretePrototypeA : Prototype
{
public override Prototype Clone()
{
return (ConcretePrototypeA)this.MemberwiseClone();
}
}

class ConcretePrototypeB : Prototype
{
public override Prototype Clone()
{
return (ConcretePrototypeB)this.MemberwiseClone();
}
}

class PrototypeManager
{
Hashtable ht = new Hashtable();
public PrototypeManager()
{
ht.Add("A", new ConcretePrototypeA());
ht.Add("B", new ConcretePrototypeB());
}

public void Add(string key, Prototype prototype)
{
ht.Add(key,prototype);
}

public Prototype Get(string key)
{
Prototype clone = null;
clone = ((Prototype)ht[key]).Clone();
return clone;
}
}
}

在实际开发中,可以将PrototypeManager设计为单例类,确保系统中有且仅有一个PrototypeManager对象,既有利于节省系统资源,还可以更地对原型管理器对象进行控制.

原型模式的优缺点与适用环境

原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中的应用较为广泛,很多软件提供的复制(Ctrl+C)和粘贴(Ctrl+V)操作就是原型模式的典型应用。

原型模式的优点

原型模式的主要优点如下:

  • (1)当要创建的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
  • (2)扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统没有任何影响。
  • (3)原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
  • (4)可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(例如恢复到某一历史状态),可辅助实现撤销操作。

原型模式的缺点

原型模式的主要缺点如下:

  • (1)需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了开闭原则。
  • (2)在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。

原型模式的适用环境

在以下情况下可以考虑使用原型模式:

  • (1)创建新对象成本较大(例如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新对象可以通过复制已有对象来获得,如果是相似对象,则可以对其成员变量稍作修改。
  • (2)系统要保存对象的状态,而对象的状态变化很小。
  • (3)需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。

本章小结

(1)在原型模式中,使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。原型模式是一种对象创建型模式。

(2)原型模式包含抽象原型类、具体原型类和客户类3个角色。其中,抽象原型类是声明克隆方法的接口,是所有具体原型类的公共父类;具体原型类实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。

(3)根据在复制原型对象的同时是否复制包含在原型对象中引用类型的成员变量,原型模式的克隆机制可分为浅克隆和深克隆。在浅克隆中,当原型对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有被复制;在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将被复制。

(4)在C#语言中,提供了一个MemberwiseClone()方法用于实现克隆,此还提供了用于充当抽象原型角色的ICloneable接口。

(5)原型模式的主要优点是当要创建的对象实例较为复杂时,可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率,而且具有较好的扩展性;其主要缺点在于需要为每一个类配备一个克隆方法,因此在对已有类进行改造时比较麻烦,需要修改源代码,并且在实现深克隆时需要编写较为复杂的代码。

(6)原型模式适用的环境:创建新对象成本较大,新对象可以通过复制已有对象来获得;系统要保存对象的状态,而对象的状态变化很小;需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。

(7)原型管理器将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象获得。

0%