Prototype Pattern
1、概述
原型模式其实不难,它主要做的事情就是对象的拷贝,即使不说原型模式我们也可以想到在对象中提供一个方法创建当前对象的实例,然后将属性一个一个set进去。而原型模式其实就是将"拷贝"这个事情提取了一下,我们可以书写一个抽象原型类
(抽象类和接口即可),其中包含clone()方法,然后使用不同的子类(具体原型类
)去实现该接口覆盖clone方法(原型方法
)即可,而clone()的实现还是我们自己书写(也可以使用java自带的方法),核心思路如下。
class ConcretePrototype implements Prototype
{
private String attr; //成员属性
public void setAttr(String attr)
{
this.attr = attr;
}
public String getAttr()
{
return this.attr;
}
public Prototype clone() //克隆方法
{
Prototype prototype = new ConcretePrototype(); //创建新对象
prototype.setAttr(this.attr);
return prototype;
}
}
思考:能否将上述代码中的clone()方法写成:public Prototype clone() { return this;}
答案是不能,因为这样我们返回的对象就是同一个了,也就算不上clone,可以打印原对象==新对象发现返回true。
2、Java自带clone
我们现在使用原型模式完成一个工作周报的创建,周报可能有多种类型,只需要在每种类型的周报entity中书写clone()方法即可。这里使用Java自带的方法进行实现,将Cloneable视作抽象原型类,实现Cloneable接口1。
//工作周报WeeklyLog:具体原型类,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class WeeklyLog implements Cloneable
{
private String name;
private String date;
private String content;
public void setName(String name) {
this.name = name;
}
public void setDate(String date) {
this.date = date;
}
public void setContent(String content) {
this.content = content;
}
public String getName() {
return (this.name);
}
public String getDate() {
return (this.date);
}
public String getContent() {
return (this.content);
}
//克隆方法clone(),此处使用Java语言提供的克隆机制
public WeeklyLog clone()
{
Object obj = null;
try
{
obj = super.clone();
return (WeeklyLog)obj;
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持复制!");
return null;
}
}
}
可以使用client测试如下:
class Client
{
public static void main(String args[])
{
WeeklyLog log_previous = new WeeklyLog(); //创建原型对象
log_previous.setName("张无忌");
log_previous.setDate("第12周");
log_previous.setContent("这周工作很忙,每天加班!");
System.out.println("****周报****");
System.out.println("周次:" + log_previous.getDate());
System.out.println("姓名:" + log_previous.getName());
System.out.println("内容:" + log_previous.getContent());
System.out.println("--------------------------------");
WeeklyLog log_new;
log_new = log_previous.clone(); //调用克隆方法创建克隆对象
log_new.setDate("第13周");
System.out.println("****周报****");
System.out.println("周次:" + log_new.getDate());
System.out.println("姓名:" + log_new.getName());
System.out.println("内容:" + log_new.getContent());
}
}
输出结果如下:
****周报****
周次:第12周
姓名:张无忌
内容:这周工作很忙,每天加班!
--------------------------------
****周报****
周次:第13周
姓名:张无忌
内容:这周工作很忙,每天加班!
思考:以下语句的输出结果是什么,为什么?
System.out.println(log_previous == log_new);
System.out.println(log_previous.getDate() == log_new.getDate());
System.out.println(log_previous.getName() == log_new.getName());
System.out.println(log_previous.getContent() == log_new.getContent());
结果如下:
false
false
true
true
1、因为返回的是不同对象(引用)所以为false(注:引用中存储的是指向对象的地址)
2、因为data被重新set了一下
3、4是因为属性拷贝后不变
3、深拷贝与浅拷贝
①浅拷贝
我们上面所说的都是浅拷贝,浅拷贝会将WeeklyLog中的属性全部clone一份到新的对象中,我们的WeeklyLog属性还比较简单,如果其中包含另一个对象会如何?我们现在在WeeklyLog中添加一个Attachment属性,是否会影响我们的拷贝结果。
//附件类
class Attachment
{
private String name; //附件名
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
public void download()
{
System.out.println("下载附件,文件名为" + name);
}
}
//工作周报WeeklyLog
class WeeklyLog implements Cloneable
{
//为了简化设计和实现,假设一份工作周报中只有一个附件对象,实际情况中可以包含多个附件,可以通过List等集合对象来实现
private Attachment attachment;
private String name;
private String date;
private String content;
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
public void setName(String name) {
this.name = name;
}
public void setDate(String date) {
this.date = date;
}
public void setContent(String content) {
this.content = content;
}
public Attachment getAttachment(){
return (this.attachment);
}
public String getName() {
return (this.name);
}
public String getDate() {
return (this.date);
}
public String getContent() {
return (this.content);
}
//使用clone()方法实现浅克隆
public WeeklyLog clone()
{
Object obj = null;
try
{
obj = super.clone();
return (WeeklyLog)obj;
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持复制!");
return null;
}
}
}
class Client
{
public static void main(String args[])
{
WeeklyLog log_previous, log_new;
log_previous = new WeeklyLog(); //创建原型对象
Attachment attachment = new Attachment(); //创建附件对象
log_previous.setAttachment(attachment); //将附件添加到周报中
log_new = log_previous.clone(); //调用克隆方法创建克隆对象
//比较周报
System.out.println("周报是否相同? " + (log_previous == log_new));
//比较附件
System.out.println("附件是否相同? " + (log_previous.getAttachment() == log_new.getAttachment()));
}
}
此时的打印结果为:
周报是否相同? false
附件是否相同? true
可以看到浅拷贝只是将属性的值或者引用地址拷贝了出来,如果只是简单的基本类型并没有关系,如果是引用类型,而我们又需要对引用对象的属性进行操作,那么及时是拷贝出来的attachment也会影响原对象的attachment,因为他们在WeeklyLog中的值是一样的,是指向同一个attachment。
注:虽然String也是引用类型,但是一般我们不会去动其中的属性,最多就是把这个引用的地址重新赋值,所以可以认为无影响。
②深拷贝
相对于浅拷贝,深拷贝就是可以将复杂对象的属性完全clone一份,及时是引用类型可以将引用类型里的属性clone一份。那么我们该如何将深层属性clone呢,有两种方式:将对象写到流中、使用json转换。
-
IO流
将对象写到io流中需要实现序列化接口,而且WeeklyLog和Attachment都需要实现。import java.io.*; //附件类 class Attachment implements Serializable { private String name; //附件名 …… }
import java.io.*; //工作周报类 class WeeklyLog implements Serializable { private Attachment attachment; private String name; …… //使用序列化技术实现深克隆 public WeeklyLog deepClone() throws IOException, ClassNotFoundException, OptionalDataException { //将对象写入流中 ByteArrayOutputStream bao=new ByteArrayOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(bao); oos.writeObject(this); //将对象从流中取出 ByteArrayInputStream bis=new ByteArrayInputStream(bao.toByteArray()); ObjectInputStream ois=new ObjectInputStream(bis); return (WeeklyLog)ois.readObject(); } }
-
使用json转换(推荐)
使用json转换比较简单方便,也可以达到同样的效果,而且不用实现序列化接口。
public WeeklyLog clone() { String json = JSON.toJSONString(this); WeeklyLog parse = (WeeklyLog) JSON.parseObject(json, WeeklyLog.class); return parse; }
4、原型管理器
其实原型模式到这里基本就结束了,但是我们还可以学习一个"原型管理器"的概念(也就是工厂),上面我们已经说过对象的clone其实是编码在对象中的,我们已经有了对象的方法能力,但是缺少对象的创建者,如果有个工厂能够帮助我们进行对象创建使用起来想必是十分方便的。因为原型模式其实是一种模板模式,它可以返回同一对象的clone,实际业务可能存在这样的情况:我们需要快速生成一批文档,这些文档都是一样的,我们很自然的想到使用原型模式生成,我们这里可以引入一个原型管理器的概念来帮助我们管理文档,直接从管理器中即可获取到clone对象,符合"单一职责原则"。
import java.util.*;
//抽象公文接口,也可定义为抽象类,提供clone()方法的实现,将业务方法声明为抽象方法
interface OfficialDocument extends Cloneable
{
public OfficialDocument clone();
public void display();
}
//可行性分析报告(Feasibility Analysis Report)类
class FAR implements OfficialDocument
{
public OfficialDocument clone()
{
OfficialDocument far = null;
try
{
far = (OfficialDocument)super.clone();
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持复制!");
}
return far;
}
public void display()
{
System.out.println("《可行性分析报告》");
}
}
//软件需求规格说明书(Software Requirements Specification)类
class SRS implements OfficialDocument
{
public OfficialDocument clone()
{
OfficialDocument srs = null;
try
{
srs = (OfficialDocument)super.clone();
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持复制!");
}
return srs;
}
public void display()
{
System.out.println("《软件需求规格说明书》");
}
}
//原型管理器(使用饿汉式单例实现)
class PrototypeManager
{
//定义一个Hashtable,用于存储原型对象
private Hashtable ht=new Hashtable();
private static PrototypeManager pm = new PrototypeManager();
//为Hashtable增加公文对象
private PrototypeManager()
{
ht.put("far",new FAR());
ht.put("srs",new SRS());
}
//增加新的公文对象
public void addOfficialDocument(String key,OfficialDocument doc)
{
ht.put(key,doc);
}
//通过浅克隆获取新的公文对象
public OfficialDocument getOfficialDocument(String key)
{
return ((OfficialDocument)ht.get(key)).clone();
}
public static PrototypeManager getPrototypeManager()
{
return pm;
}
}
5、总结
①优点
- 如果实例的创建比较复杂,使用原型模式复制一个可以提高创建效率。
- 扩展性较好,由于使用了抽象原型类,所以我们完全可以使用配置文件的方式切换具体原型类,并且增加、减少具体类对代码几乎没有影响。
- 原型模式的工厂更加简化,像工厂方法模式必须提供一个对产品对应的工厂类。
- 可以将对象以深克隆的方式保存,以便需要时使用(如:撤销操作)。
②缺点
- 每一个类一个clone方法,修改需要改动源代码,违反了“开闭原则”。
- 深克隆代码较为复杂,如果对象很复杂,每一个还要去实现序列化接口。(当然使用json的方式就不是问题)
③使用场景
- 创建新对象成本较大时,如初始化需要占用较长的时间,占用太多的CPU资源或网络资源。
- 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。 ↩︎