一、原型模式介绍
原型模式介绍
原型模式(Prototype Pattern)是一种对象创建型模式,它是使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。
如上图的悟空,一缕猴毛能七十二变。
如果你有一个对象, 并希望生成与其完全相同的一个复制品, 你该如何实现呢? 首先, 你必须新建一个属于相同类的对象。 然后, 你必须遍历原始对象的所有成员变量, 并将成员变量值复制到新对象中。
不错! 但有个小问题。 并非所有对象都能通过这种方式进行复制, 因为有些对象可能拥有私有成员变量, 它们在对象本身以外是不可见的。
“从外部” 复制对象并非总是可行。
直接复制还有另外一个问题。 因为你必须知道对象所属的类才能创建复制品, 所以代码必须依赖该类。 即使你可以接受额外的依赖性, 那还有另外一个问题: 有时你只知道对象所实现的接口, 而不知道其所属的具体类, 比如可向方法的某个参数传入实现了某个接口的任何对象。
二、模式解决方案
模式解决方案
原型模式将克隆过程委派给被克隆的实际对象,模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合,通常情况下, 这样的接口中仅包含一个 克隆方法。
所有的类对克隆方法的实现都非常相似,该方法会创建一个当前类的对象, 然后将原始对象所有的成员变量值复制到新建的类中。 你甚至可以复制私有成员变量, 因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量。
支持克隆的对象即为原型。 当你的对象有几十个成员变量和几百种类型时, 对其进行克隆甚至可以代替子类的构造。
预生成原型可以代替子类的构造。
其运作方式如下: 创建一系列不同类型的对象并不同的方式对其进行配置,如果所需对象与预先配置的对象相同, 那么你只需克隆原型即可, 无需新建一个对象。
三、真实世界类比
真实世界类比
现实生活中, 产品在得到大规模生产前会使用原型进行各种测试,但在这种情况下, 原型只是一种被动的工具, 不参与任何真正的生产活动。
一个细胞的分裂。
由于工业原型并不是真正意义上的自我复制, 因此细胞有丝分裂 (还记得生物学知识吗?) 或许是更恰当的类比。 有丝分裂会产生一对完全相同的细胞。 原始细胞就是一个原型, 它在复制体的生成过程中起到了推动作用。
注意事项
与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的,浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。
四、原型模式动机
原型模式动机
在软件系统中,有时候需要多次创建某一类型的对象,为了简化创建过程,可以只创建一个对象,然后再通过克隆的方式复制出多个相同的对象,这就是原型模式的设计思想。
在面向对象系统中,使用原型模式来复制一个对象自身,从而克隆出多个与原型对象一摸一样的对象。
在软件系统中,有些对象的创建过程较为复杂,而且有时候需要频繁创建,类似JavaWeb,数据库连接池等。原型模式通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象,这就是原型模式的意图所在。
五、原型模式结构
原型模式结构
原型模式定义:原型模式是一种对象创建型模型,用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。原型模式允许一个对象再创建另外一个可定制的对象,无须知道任何创建的细节,原型模式的基本工作原理是通过讲一个原型对象传给那个那发动创建的对象,这个要发动创建的对象通过请求原型对象复制原型来实现创建过程。
原型模式包含以下3个角色:
Prototype(抽象原型类)
它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口,甚至还可以是具体实现类。
ConcretePrototype(具体原型类)
它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
Client(客户类)
在客户类中,让一个原型对象克隆自身从而创建一个新的对象,只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。
六、模式浅拷贝与深拷贝
模式浅拷贝与深拷贝
根据在复制原型对象的同时是否复制包含在原型对象中引用类型的成员变量,原型模式的克隆机制可分为两种,即浅克隆(Shallow Clone)和深克隆(Deep Clone)。
浅克隆
对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性复制一份给新的对象,因为是两份不同的数据,所以对其中一的对象的成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
对于数据类型是引用类型的成员变量,比如说成员变量是某个数组,某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用指(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
深克隆
不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,知道该对象可达的所有对象。
也就是说,对象进行深拷贝要对整个对象图进行拷贝,简单的说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。在一个对象中修改该成员变量不会影响到另一个对象的该成员变量值。
七、通用实现方法
实现原型模式的关键在于如何实现克隆方法,这里介绍两种在Java语言中最常用的克隆实现方法。
通用实现方法
通用的克隆实现方法是在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其返回,同时将相关的参数传入新创建的对象中,保证它们的成员变量相同。
典型的抽象原型类代码如下:
public abstract class Prototype
{
public abstract Prototype clone();
}
典型的具体原型类代码如下:
public class ConcretePrototype extends Prototype
{
private String name; // 成员变量
public void setName(String name)
{
this.name = name;
}
public void getName()
{
return this.name;
}
// 克隆方法实现
public Prototype clone()
{
Prototype prototype = new ConcretePrototype(); // 创建新对象
prototype.setName(this.name);
return prototype;
}
}
这样我们就只需要在客户类中创建一个ConcretePrototype对象作为原型对象,然后调用其clone()方法即可得到对应的克隆对象。
此方法是原型模式的通用实现,它与编程语言本身的特性无关,其他面向对象编程语言也可以使用这种形式来实现对原型对象的克隆。
在这种通用实现方法中,可通过手工编写clone()方法来实现浅克隆和深克隆,对于引用类型的对象,可以在clone()方法中通过赋值的方式来实现复制,这是一种浅克隆实现方案;如果在clone()方法中通过创建一个全新的成员对象来实现复制,则是一种深克隆实现方案。
八、在Java语言中的clone()方法和Cloneable接口
在Java语言中的clone()方法和Cloneable接口
在Java语言中,所有的Java类均继承自java.lang.Object类,Object类提供了一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供clone()方法来实现对象的浅克隆。
需要注意的是能够实现克隆的Java类都必须实现一个标识接口Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将会抛出一个CloneNotSupportedException异常,如下代码所示:
public class ConcretePrototype implements Cloneable
{
public Prototype clone()
{
Object object = null;
try
{
object = super.clone(); // 浅克隆
}
catch (CloneNotSupportedException exception)
{
System.err.println("Not support Cloneable");
}
return (Prototype)object;
}
}
为了获取对象的一个克隆,可以直接利用Object类的clone()方法,其具体步骤如下:
1、在派生类中覆盖基类的clone()方法,并声明为public。
2、在派生类的clone()方法中调用super.clone()。
3、派生类需实现Cloneable接口。
此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类。
九、模式应用举例
模式应用举例
题目描述
某数据处理软件需要增加一个图表复制功能,在图表对象(DataChart)中包含一个数据集对象(DataSet)。数据集对象用于封装要显示的数据,用户可以通过界面上的复制按钮将该图表复制一份,复制后,即可得到新的图表对象,然后可以修改新图表的编号、颜色、数据。试用原型模式设计软件实现深克隆。
UML类图
在该设计方案中,DataChart 类包含一个 DataSet 对象,在复制 DataChart 对象的同时将
复制 DataSet 对象,因此需要使用深克隆技术,可使用流来实现深克隆,其中Serializable是java.io包中定义的、用于实现Java类的序列化操作而提供的一个语义级别的接口。Serializable序列化接口没有任何方法或者字段,只是用于标识可序列化的语义,实现了Serializable接口的类可以被ObjectOutputStream转换为字节流,同时也可以通过ObjectInputStream再将其解析为对象。故我们实现这个接口即可使用流来实现深克隆
原型管理器
原型管理器实现
原型管理器(Prototype Manager)是将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象, 如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。 在原型管理器中针对抽象原型类进行编程,以便扩展。 其结构如图所示:
其中典型的原型管理器PrototypeManager类的实现代码片段如下:
package prototype_pattern;
import java.util.Hashtable;
/**
* @author Cnc_hzf
* @date 2022/4/22 15:00
*/
public class PrototypeManager
{
// 使用Hashtable存储原型对象
private Hashtable prototypeTable = new Hashtable();
public PrototypeManager()
{
prototypeTable.put("A", new ConcretePrototypeA());
prototypeTable.put("B", new ConcretePrototypeB());
}
public void add(String key, Prototype prototype)
{
prototypeTable.put(key, prototype);
}
public Prototype get(String key)
{
// 通过克隆方法创建新对象
Prototype clone = ((Prototype) prototypeTable.get(key)).clone();
return clone;
}
}
在实际开发中可以将PrototypeManger设计为单例类,确保系统中有且仅有一个PrototypeManager对象,这样既有利于节省系统资源,还可以更好地对原型管理器对象进行控制。
原型管理器应用举例
题目描述
某公司需要创建一个公文管理器,公文管理器中需要提供一个集合对象来存储一些公文模板,用户可以通过复制这些模板快速的创建新的公文,试使用带有原型管理器的原型模式来设计该公文管理器并使用Java代码编程模拟。
UML类图
其中,OfficialDocument (抽象公文类)充当抽象原型类,其子类 FAR(Feasibility Analysis
Report,可行性分析报告)和 SRS(Software Requirements Specification,软件需求规格说明书)充当具体原型类,PrototypeManager 充当原型管理器。
十、原型语言范式
原型语言范式
很多人认为“面向对象编程”和“类”是同义词,OOP的定义却让人感觉正好相反, 毫无疑问,OOP让你定义“对象”,将数据和代码绑定在一起。 与C这样的结构化语言相比,与Scheme这样的函数语言相比, OOP的特性是它将状态和行为紧紧地绑在一起。
你也许认为类是完成这个的唯一方式方法, 但是包括Dave Ungar和Randall Smith的一大堆家伙一直在拼命区分OOP和类。 他们在80年代创建了一种叫做Self的语言。它不用类实现了OOP。
Self语言
就单纯意义而言,Self比基于类的语言更加面向对象,我们认为OOP将状态和行为绑在一起,但是基于类的语言实际将状态和行为割裂开来。
拿你最喜欢的基于类的语言的语法来说,为了接触对象中的一些状态,你需要在实例的内存中查询,状态包含在实例中。
但是,为了调用方法,你需要找到实例的类, 然后在那里调用方法,行为包含在类中,获得方法总需要通过中间层,这意味着字段和方法是不同的。
一个类,包含了一系列方法。一个实例,包含了一系列字段和指向类的指针。
举个例子,为了调用C++中的虚方法,你需要在实例中找指向虚方法表的指针,然后再在那里找方法。
Self结束了这种分歧。无论你要找啥,都只需在对象中找。 实例同时包含状态和行为,你可以构建拥有完全独特方法的对象。
如果这就是Self语言的全部,那它将很难使用,基于类的语言中的继承,不管有多少缺陷,总归提供了有用的机制来重用代码,避免重复,为了不使用类而实现一些类似的功能,Self语言加入了委托。
如果要在对象中寻找字段或者调用方法,首先在对象内部查找,如果能找到,那就成了,如果找不到,在对象的父对象中寻找。 这里的父类仅仅是一个对其他对象的引用,当我们没能在第一个对象中找到属性,我们尝试它的父对象,然后父类的父对象,继续下去直到找到或者没有父对象为止,换言之,失败的查找被委托给对象的父对象。
我在这里简化了,Self实际上支持多个父对象,父对象只是特别标明的字段,意味着你可以继承它们或者在运行时改变他们, 你最终得到了“动态继承”。
一个对象包含了字段和方法,以及一个指向委托对象的指针。
父对象让我们在不同对象间重用行为(还有状态!),这样就完成了类的公用功能。 类做的另一个关键事情就是给出了创建实例的方法,当你需要新的某物,你可以直接new Thingamabob(),或者随便什么你喜欢的表达法,类是实例的生产工厂。
不用类,我们怎样创建新的实例? 特别地,我们如何创建一堆有共同点的新东西? 就像这个设计模式,在Self中,达到这点的方式是使用克隆。
在Self语言中,就好像每个对象都自动支持原型设计模式,任何对象都能被克隆,为了获得一堆相似的对象:
1、将对象塑造成你想要的状态,你可以直接克隆系统内建的基本Object,然后向其中添加字段和方法。
2、克隆它来产出……额……随你想要多少就克隆多少个对象。
无需烦扰自己实现clone();我们就实现了优雅的原型模式,原型被内建在系统中。
这个系统美妙,灵巧,而且小巧, 一听说它,我就开始创建一个基于原型的语言来进一步学习。
十一、为数据模型构建原型
为数据模型构建原型
随着编程的进行,如果你比较程序与数据的字节数, 那么你会发现数据的占比稳定地增长,早期的游戏在程序中生成几乎所有东西,这样程序可以塞进磁盘和老式游戏卡带,在今日的游戏中,代码只是驱动游戏的“引擎”,游戏是完全由数据定义的。
这很好,但是将内容推到数据文件中并不能魔术般地解决组织大项目的挑战,它只能把这挑战变得更难,我们使用编程语言就因为它们有办法管理复杂性。
不再是将一堆代码拷来拷去,我们将其移入函数中,通过名字调用,不再是在一堆类之间复制方法,我们将其放入单独的类中,让其他类可以继承或者组合。
当游戏数据达到一定规模时,你真的需要考虑一些相似的方案,我不指望在这里能说清数据模式这个问题, 但我确实希望提出个思路,让你在游戏中考虑考虑:使用原型和委托来重用数据。
假设我们为早先提到的山寨版《圣铠传说》定义数据模型,游戏设计者需要在很多文件中设定怪物和物品的属性。
一个常用的方法是使用JSON。 数据实体一般是字典,或者属性集合,或者其他什么术语, 因为程序员就喜欢为旧事物发明新名字。
所以游戏中的哥布林也许被定义为像这样的东西:
{
"name": "goblin grunt",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"]
}
这看上去很易懂,哪怕是最讨厌文本的设计者也能使用它,所以,你可以给哥布林大家族添加几个兄弟分支:
{
"name": "goblin wizard",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"],
"spells": ["fire ball", "lightning bolt"]
}
{
"name": "goblin archer",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"],
"attacks": ["short bow"]
}
现在,如果这是代码,我们会闻到了臭味,在实体间有很多的重复,训练优良的程序员讨厌重复。 它浪费了空间,消耗了作者更多时间,你需要仔细阅读代码才知道这些数据是不是相同的。 这难以维护,如果我们决定让所有哥布林变强,需要记得将三个哥布林都更新一遍。糟糕糟糕糟糕。
如果这是代码,我们会为“哥布林”构建抽象,并在三个哥布林类型中重用,但是无能的JSON没法这么做,所以让我们把它做得更加巧妙些。
我们可以为对象添加"prototype"字段,记录委托对象的名字,如果在此对象内没找到一个字段,那就去委托对象中查找。
这让"prototype"不再是数据,而成为了元数据。 哥布林有绿色疣皮和黄色牙齿,它们没有原型,原型是表示哥布林的数据模型的属性,而不是哥布林本身的属性。
这样,我们可以简化我们的哥布林JSON内容:
{
"name": "goblin grunt",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"]
}
{
"name": "goblin wizard",
"prototype": "goblin grunt",
"spells": ["fire ball", "lightning bolt"]
}
{
"name": "goblin archer",
"prototype": "goblin grunt",
"attacks": ["short bow"]
}
由于弓箭手和术士都将grunt作为原型,我们就不需要在它们中重复血量,防御和弱点,我们为数据模型增加的逻辑超级简单——基本的单一委托——但已经成功摆脱了一堆冗余。
有趣的事情是,我们没有更进一步,把哥布林委托的抽象原型设置成“基本哥布林”,相反,我们选择了最简单的哥布林,然后委托给它。
在基于原型的系统中,对象可以克隆产生新对象是很自然的, 我认为在这里也一样自然,这特别适合记录那些只有一处不同的实体的数据。
想想Boss和其他独特的事物,它们通常是更加常见事物的重新定义, 原型委托是定义它们的好方法,断头魔剑,就是一把拥有加成的长剑,可以像下面这样表示:
{
"name": "Sword of Head-Detaching",
"prototype": "longsword",
"damageBonus": "20"
}
只需在游戏引擎上多花点时间,你就能让设计者更加方便地添加不同的武器和怪物,而增加的这些丰富度能够取悦玩家。
十二、再来一个实例
再来一个实例
我们将创建一个抽象类 Shape 和扩展了 Shape 类的实体类,下一步是定义类 ShapeCache,该类把 shape 对象存储在一个 Hashtable 中,并在请求的时候返回它们的克隆。
PrototypePatternDemo 类使用 ShapeCache 类来获取 Shape 对象:
步骤 1
创建一个实现了 Cloneable 接口的抽象类。
Shape.java
public abstract class Shape implements Cloneable
{
private String id;
protected String type;
abstract void draw();
public String getType()
{
return type;
}
public String getId()
{
return id;
}
public void setId(String id)
{
this.id = id;
}
public Object clone()
{
Object clone = null;
try
{
clone = super.clone();
}
catch (CloneNotSupportedException e)
{
e.printStackTrace();
}
return clone;
}
}
步骤 2
创建扩展了上面抽象类的实体类。
Rectangle.java
public class Rectangle extends Shape
{
public Rectangle()
{
type = "Rectangle";
}
@Override
public void draw()
{
System.out.println("Inside Rectangle::draw() method.");
}
}
Square.java
public class Square extends Shape
{
public Square()
{
type = "Square";
}
@Override
public void draw()
{
System.out.println("Inside Square::draw() method.");
}
}
Circle.java
public class Circle extends Shape
{
public Circle()
{
type = "Circle";
}
@Override
public void draw()
{
System.out.println("Inside Circle::draw() method.");
}
}
步骤 3
创建一个类,从数据库获取实体类,并把它们存储在一个 Hashtable 中。
ShapeCache.java
import java.util.Hashtable;
public class ShapeCache
{
private static Hashtable<String, Shape> shapeMap = new Hashtable<String, Shape>();
public static Shape getShape(String shapeId)
{
Shape cachedShape = shapeMap.get(shapeId);
return (Shape) cachedShape.clone();
}
// 对每种形状都运行数据库查询,并创建该形状
// shapeMap.put(shapeKey, shape);
// 例如,我们要添加三种形状
public static void loadCache()
{
Circle circle = new Circle();
circle.setId("1");
shapeMap.put(circle.getId(),circle);
Square square = new Square();
square.setId("2");
shapeMap.put(square.getId(),square);
Rectangle rectangle = new Rectangle();
rectangle.setId("3");
shapeMap.put(rectangle.getId(),rectangle);
}
}
步骤 4
PrototypePatternDemo 使用 ShapeCache 类来获取存储在 Hashtable 中的形状的克隆。
PrototypePatternDemo.java
public class PrototypePatternDemo
{
public static void main(String[] args)
{
ShapeCache.loadCache();
Shape clonedShape = (Shape) ShapeCache.getShape("1");
System.out.println("Shape : " + clonedShape.getType());
Shape clonedShape2 = (Shape) ShapeCache.getShape("2");
System.out.println("Shape : " + clonedShape2.getType());
Shape clonedShape3 = (Shape) ShapeCache.getShape("3");
System.out.println("Shape : " + clonedShape3.getType());
}
}
步骤 5
执行程序,输出结果:
Shape : Circle
Shape : Square
Shape : Rectangle
十三、原型模式场景
模式使用场景
1、资源优化场景
2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等
3、性能和安全要求的场景
4、通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式
5、一个对象多个修改者的场景
6、一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用
7、在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用
模式应用场景
原型模式通常适用于以下场景:
1、对象之间相同或相似,即只是个别的几个属性不同的时候
2、创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源
3、创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性
4、系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值
十四、实现方式归纳
实现方式归纳
1、创建原型接口,并在其中声明克隆方法
如果你已有类层次结构,则只需在其所有类中添加该方法即可。
2、原型类必须另行定义一个以该类对象为参数的构造函数
构造函数必须复制参数对象中的所有成员变量值到新建实体中,如果你需要修改子类,则必须调用父类构造函数,让父类复制其私有成员变量值。
如果编程语言不支持方法重载,那么你可能需要定义一个特殊方法来复制对象数据,在构造函数中进行此类处理比较方便,因为它在调用new运算符后会马上返回结果对象。
3、克隆方法通常只有一行代码: 使用 new运算符调用原型版本的构造函数
注意,每个类都必须显式重写克隆方法并使用自身类名调用new运算符,否则,克隆方法可能会生成父类的对象。
4、你还可以创建一个中心化原型注册表,用于存储常用原型
你可以新建一个工厂类来实现注册表, 或者在原型基类中添加一个获取原型的静态方法。 该方法必须能够根据客户端代码设定的条件进行搜索,搜索条件可以是简单的字符串, 或者是一组复杂的搜索参数。找到合适的原型后,注册表应对原型进行克隆,并将复制生成的对象返回给客户端。
最后还要将对子类构造函数的直接调用替换为对原型注册表工厂方法的调用。
十五、原型模式优缺点
原型模式优点
1、当创建新当对象实例比较复杂时,使用原型模式可以简化对象当创建过程,通过一个已有实例可以提高新实例的创建效率
2、可以动态增加或减少产品类。由于创建产品类实例的方法是产品类(具体原型类)内部具有的,因此增加新产品对整个结构没有影响。在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响
3、原型模式提供了简化对创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的clone()方法来实现的,无须专门的工厂类来创建产品
4、可以使用深克隆的方式保存对象的状态。使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态)
原型模式缺点
1、需要为每一个类配置一个克隆方法,而且这个克隆方法需要对类对功能进行通盘考虑,这对全新对代码的类来说不是很难,但对已有对类进行改造时,不一定是一件容易对事,必须修改其源代码,违背类“开闭原则”
2、在实现深克隆时需要编写较为复杂对代码