1、什么是原型模式?
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式,简称原型模式。
2、为什么要使用原型模式?
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。
3、例子
3.1、简历(简单)
以简历为例,假设要求有一个简历类,要有姓名、性别、年龄和工作年限,一式三份。
PS:接下来我会以版本的演进来跟你说明原型模式
Version1(重复实例化)
按照常理,设计一个简历类,重复实例化3次即可。
public class Main {public static void main(String[] args) {Resume a = new Resume("小A");a.SetPersonalInfo("男", "29");a.SetWorkExperience("1998-2001", "XX公司");System.out.println(a);Resume b = new Resume("小A");b.SetPersonalInfo("男", "29");b.SetWorkExperience("1997-2002", "XX公司");System.out.println(b);Resume c = new Resume("小A");c.SetPersonalInfo("男", "29");c.SetWorkExperience("1996-2003", "XX公司");System.out.println(c);}}class Resume {private String name;private String sex;private String age;private String timeArea;private String company;public Resume(String name) {this.name = name;}//设置个人信息public void SetPersonalInfo(String sex, String age) {this.sex = sex;this.age = age;}//设置工作经历public void SetWorkExperience(String timeArea, String company) {this.timeArea = timeArea;this.company = company;}//输出简历的信息@Overridepublic String toString() {return "Resume{" +"name='" + name + '\'' +", sex='" + sex + '\'' +", age='" + age + '\'' +", timeArea='" + timeArea + '\'' +", company='" + company + '\'' +'}';}}

Version2 (通用原型模式)
在 version1 中三份简历需要三次实例化,这样的客户端代码写起来是很麻烦的,如果要二十份,你就需要二十次实例化。并且,如果我希望单独修改其中的N份简历,比如98年改成99年,那就要改N次。
为了解决这个问题,接下来我们以典型的原型模式来解决这个问题。
以下这种方法可作为原型模式的通用实现,它与编程语言特性无关,任何面向对象语言都可以使用这种形式来实现对原型的克隆。
public class Main {public static void main(String[] args) {Resume resume = new Resume();resume.setName("小A");resume.setAge("27");resume.setSex("男");resume.setTimeArea("2002-2005");resume.setCompany("君合律师事务所");Resume resume1 = (Resume) resume.clone();Resume resume2 = (Resume) resume.clone();Resume resume3 = (Resume) resume.clone();Resume resume4 = (Resume) resume.clone();Resume resume5 = (Resume) resume.clone();System.out.println(resume);System.out.println(resume1);System.out.println(resume2);System.out.println(resume3);System.out.println(resume4);System.out.println(resume5);}}/*** 抽象原型类*/abstract class Prototype {@Overridepublic abstract Prototype clone();}/*** 具体原型类*/class Resume extends Prototype {private String name;private String sex;private String age;private String timeArea;private String company;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getSex() {return sex;}public void setSex(String sex) {this.sex = sex;}public String getAge() {return age;}public void setAge(String age) {this.age = age;}public String getTimeArea() {return timeArea;}public void setTimeArea(String timeArea) {this.timeArea = timeArea;}public String getCompany() {return company;}public void setCompany(String company) {this.company = company;}//输出简历的信息@Overridepublic String toString() {return "Resume{" +"name='" + name + '\'' +", sex='" + sex + '\'' +", age='" + age + '\'' +", timeArea='" + timeArea + '\'' +", company='" + company + '\'' +'}';}@Overridepublic Prototype clone() {Resume resume = new Resume();resume.setName(this.name);resume.setAge(this.age);resume.setSex(this.sex);resume.setTimeArea(this.timeArea);resume.setCompany(this.company);return resume;}}
Version3(java 的原型模式)
通过 Version2 的实现方式,你应该已经理解了原型模式。下面我以 Version3 这个版本来说明在 Java 中如何实现原型模式。
在 Java 中,所有的类都继承自 java.lang.Object,事实上,Object 类提供一个了 protect 的 clone() 方法,可以将一个Java对象复制一份。因此在Java中可以直接使用 Object 提供的 clone() 方法来实现对象的克隆,Java语言中的原型模式实现很简单,就是实现 Java 提供的 Cloneable 接口即可。
PS:在 Version 4/5 ,我会解释什么是浅拷贝和深拷贝
public class Main {public static void main(String[] args) throws CloneNotSupportedException {Resume resume = new Resume();resume.setName("小A");resume.setAge("27");resume.setSex("男");resume.setTimeArea("2002-2005");resume.setCompany("君合律师事务所");Resume resume1 = (Resume) resume.clone();Resume resume2 = (Resume) resume.clone();Resume resume3 = (Resume) resume.clone();Resume resume4 = (Resume) resume.clone();Resume resume5 = (Resume) resume.clone();System.out.println(resume);System.out.println(resume1);System.out.println(resume2);System.out.println(resume3);System.out.println(resume4);System.out.println(resume5);}}/*** 简历类,实现 Cloneable 接口*/class Resume implements Cloneable {private String name;private String sex;private String age;private String timeArea;private String company;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getSex() {return sex;}public void setSex(String sex) {this.sex = sex;}public String getAge() {return age;}public void setAge(String age) {this.age = age;}public String getTimeArea() {return timeArea;}public void setTimeArea(String timeArea) {this.timeArea = timeArea;}public String getCompany() {return company;}public void setCompany(String company) {this.company = company;}//输出简历的信息@Overridepublic String toString() {return "Resume{" +"name='" + name + '\'' +", sex='" + sex + '\'' +", age='" + age + '\'' +", timeArea='" + timeArea + '\'' +", company='" + company + '\'' +'}';}@Overridepublic Object clone() throws CloneNotSupportedException {return super.clone();}}
Version4(原型模式:浅拷贝)
在 Version4 中,我为简历类新增了一个附件类作为属性,以此来解释什么是浅拷贝。
public class Main {public static void main(String[] args) throws CloneNotSupportedException {Attachment attachment = new Attachment("介绍信:XXXX", "blog.xxx.com");Resume resume = new Resume();resume.setName("小A");resume.setAge("27");resume.setSex("男");resume.setTimeArea("2002-2005");resume.setCompany("君合律师事务所");resume.setAttachment(attachment);Resume resumeCopy = (Resume) resume.clone();System.out.println(resume);System.out.println(resumeCopy);if (resume == resumeCopy) {System.out.println("两份简历是同一个对象");} else {System.out.println("两份简历不是同一个对象");}if (resume.getAttachment() == resumeCopy.getAttachment()) {System.out.println("两份简历中的附件是同一个对象");} else {System.out.println("两份简历中的附件不是同一个对象");}}}/*** 简历附件类*/class Attachment {// 介绍信private String letterIntroduction;// 博客地址private String blogAddress;public Attachment(String letterIntroduction, String blogAddress) {this.letterIntroduction = letterIntroduction;this.blogAddress = blogAddress;}public String getLetterIntroduction() {return letterIntroduction;}public void setLetterIntroduction(String letterIntroduction) {this.letterIntroduction = letterIntroduction;}public String getBlogAddress() {return blogAddress;}public void setBlogAddress(String blogAddress) {this.blogAddress = blogAddress;}@Overridepublic String toString() {return "Attachment{" +"letterIntroduction='" + letterIntroduction + '\'' +", blogAddress='" + blogAddress + '\'' +'}';}}/*** 简历类,实现 Cloneable 接口*/class Resume implements Cloneable {private String name;private String sex;private String age;private String timeArea;private String company;private Attachment attachment;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getSex() {return sex;}public void setSex(String sex) {this.sex = sex;}public String getAge() {return age;}public void setAge(String age) {this.age = age;}public String getTimeArea() {return timeArea;}public void setTimeArea(String timeArea) {this.timeArea = timeArea;}public String getCompany() {return company;}public void setCompany(String company) {this.company = company;}public Attachment getAttachment() {return attachment;}public void setAttachment(Attachment attachment) {this.attachment = attachment;}//输出简历的信息@Overridepublic String toString() {return "Resume{" +"name='" + name + '\'' +", sex='" + sex + '\'' +", age='" + age + '\'' +", timeArea='" + timeArea + '\'' +", company='" + company + '\'' +", attachment=" + attachment +'}';}@Overridepublic Object clone() throws CloneNotSupportedException {return super.clone();}}

根据控制台输出的信息,可以知道目前按照 Version4 的方式实现的原型模式有问题。clone 简历类时,简历类中的附件类并没有被完全复制,而是拷贝了一份引用而已,实际上指向的还是同一个附件。
PS:Java 的基础知识
这就是浅拷贝。
Version5(原型模式:深拷贝)
为了解决 Version4 的简历附件问题(浅拷贝),所以有了 Version5 ,我会以这个版本来解释什么是深拷贝。
要实现对简历类的深拷贝,有两种方案:
1)拷贝所有的引用类型属性
直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。
2)序列化
先将对象序列化,然后再反序列化成新的对象。
public Object deepCopy(Object object) {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(object);
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return oi.readObject();
}
Version5 采用的是第1种方案。先让简历类中的所有引用类型属性类去实现 clonable 接口,并在简历类中的 clone 方法将该引用类型的属性进行替换。
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Attachment attachment = new Attachment("介绍信:XXXX", "blog.xxx.com");
Resume resume = new Resume();
resume.setName("小A");
resume.setAge("27");
resume.setSex("男");
resume.setTimeArea("2002-2005");
resume.setCompany("君合律师事务所");
resume.setAttachment(attachment);
Resume resumeCopy = (Resume) resume.clone();
System.out.println(resume);
System.out.println(resumeCopy);
if (resume == resumeCopy) {
System.out.println("两份简历是同一个对象");
} else {
System.out.println("两份简历不是同一个对象");
}
if (resume.getAttachment() == resumeCopy.getAttachment()) {
System.out.println("两份简历中的附件是同一个对象");
} else {
System.out.println("两份简历中的附件不是同一个对象");
}
}
}
/**
* 简历附件类
*/
class Attachment implements Cloneable {
// 介绍信
private String letterIntroduction;
// 博客地址
private String blogAddress;
public Attachment(String letterIntroduction, String blogAddress) {
this.letterIntroduction = letterIntroduction;
this.blogAddress = blogAddress;
}
public String getLetterIntroduction() {
return letterIntroduction;
}
public void setLetterIntroduction(String letterIntroduction) {
this.letterIntroduction = letterIntroduction;
}
public String getBlogAddress() {
return blogAddress;
}
public void setBlogAddress(String blogAddress) {
this.blogAddress = blogAddress;
}
@Override
public String toString() {
return "Attachment{" +
"letterIntroduction='" + letterIntroduction + '\'' +
", blogAddress='" + blogAddress + '\'' +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
/**
* 简历类,实现 Cloneable 接口
*/
class Resume implements Cloneable {
private String name;
private String sex;
private String age;
private String timeArea;
private String company;
private Attachment attachment;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getTimeArea() {
return timeArea;
}
public void setTimeArea(String timeArea) {
this.timeArea = timeArea;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
public Attachment getAttachment() {
return attachment;
}
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
//输出简历的信息
@Override
public String toString() {
return "Resume{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age='" + age + '\'' +
", timeArea='" + timeArea + '\'' +
", company='" + company + '\'' +
", attachment=" + attachment +
'}';
}
@Override
public Object clone() throws CloneNotSupportedException {
Resume obj = (Resume) super.clone();
obj.setAttachment((Attachment) this.attachment.clone());
return obj;
}
}

根据控制台输出的信息,可以看到 Version4 中的问题已经解决了,不同的简历中携带的附件已经不是指向同一个对象了。
4、总结
4.1、原型模式的优缺点
1)优点
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,可以以达到节省创建时间的目的。
并且,客户端如果需要修改部分对象,可以先改一个然后再 clone ,避免了一个一个对象修改的麻烦。
2)缺点
待续
4.2、原型模式的两种实现方式
原型模式有两种实现方法,深拷贝和浅拷贝。
浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。
如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。除非像从数据库中加载 10 万条数据并构建散列表索引,操作非常耗时,这种情况下比较推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝。
4.3、关于原型模式的一些问题
1)何为“对象的创建成本比较大”?
实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。
但是,如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。
