此系列文章为清华大学出版社出版刘伟编著《Java设计模式》的学习笔记。
1 概述
原型模式是一种对象创建型模式,它的工作原理很简单:将一个原型对象传给要发动创建的对象(即客户端对象),这个发动创建的对象通过请求原型对象复制自己来实现创建过程。
2 结构与实现
2.1 原型模式结构
原型模式包含 3 个角色:
- AbstractPrototype (抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口,甚至还可以是具体实现类。
- ConcretePrototype (具体圆形类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回一个自己的克隆对象。
- Client (客户类):在客户类中,让一个原型对象克隆自身从而创建一个新的对象,只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类编程,因此用户可以根据需要选择具体的原型类,系统具有较好的可扩展性,增加或更换具体的原型类都很方便。
2.2 浅克隆与深克隆
根据在复制原型对象的同时,是否复制包含在原型对象中的引用对象成员,原型模式的克隆机制分为浅克隆和深克隆。
2.2.1 浅克隆
浅克隆与深克隆的讨论针对的是原型类中的引用对象成员,而浅克隆的特点是不会在内存中新建一个引用对象成员,与原型类共用该引用对象成员,在原型类执行浅克隆时,向克隆产生的新对象传递的是原型对象的引用对象的地址,造成两个类共享该引用成员变量。
2.2.2 深克隆
与浅克隆相反,深克隆在内存中重新申请空间,克隆一个不和原型类共享的引用成员变量。
2.2.3 Java 中的 clone() 方法和 Cloneable 接口
Java 中,java.lang.Object 类提供了一个 clone() 方法,可以将一个Java对象复制一份。因此在 Java 中可以直接使用该 clone() 方法来实现对象的浅克隆。
需要注意的是,能够实现克隆的Java类必须实现一个标识接口 Cloneable,表示这个 Java 类支持被复制。如果一个类没有实现这个接口但是调用了 clone() 方法,Java 编译器将会抛出一个 CloneNotSupportedException 异常。
Java 中的 clone() 方法满足以下几点:
- 对任何对象 x,都有 x.clone() != x ,即克隆对象与原型对象不是同一个对象。
- 对任何对象 x,都有 x.clone().getClass() == x.getClass() ,即克隆对象与原型对象的类型一样。
- 如果对象 x 的 equals() 方法定义恰当,那么 x.clone().equals(x) 应该成立。
利用 Object 类的 clone() 方法,需要以下几个步骤:
- 在子类中覆盖父类的 clone() 方法,并声明为 public。
- 在子类的 clone() 方法中调用 super.clone()。
- 子类需要实现 Cloneable 接口。
此时,Object 类相当于抽象原型类,所有实现了 Cloneable 接口的类相当于原型类。
2.2.4 Java 中的 Serializable 接口
实现深克隆的方式有很多,当我们利用字节流来实现深克隆时,原型类需要实现 Serializable 接口。
2.3 原型模式举例
一、背景介绍
在使用某 OA 系统时,有些岗位的员工发现他们每周的工作都大同小异,因此在填写工作周报时很多内容都是重复的,为了提高工作周报的创建效率,大家迫切希望有一种机制能够快速创建相同或者相似的周报,包括创建周报的附件。请使用原型模式实现。
二、项目结构
三、抽象原型类
package CreationalPattern.PrototypePattern.AbstractPrototype;
import CreationalPattern.PrototypePattern.Attachment;
import java.io.IOException;
import java.io.Serializable;
public abstract class WeeklyLog implements Cloneable, Serializable {
protected Attachment attachment;
protected String name;
protected String date;
protected String content;
public abstract Attachment getAttachment();
public abstract void setAttachment(Attachment attachment);
public abstract String getName();
public abstract void setName(String name);
public abstract String getDate();
public abstract void setDate(String date);
public abstract String getContent();
public abstract void setContent(String content);
//todo:shadowClone()方法实现浅拷贝
public abstract WeeklyLog shadowClone();
//todo:deepClone()方法实现深拷贝
public abstract WeeklyLog deepClone() throws IOException, ClassNotFoundException;
}
四、具体原型类
package CreationalPattern.PrototypePattern.ConcretePrototype;
import CreationalPattern.PrototypePattern.AbstractPrototype.WeeklyLog;
import CreationalPattern.PrototypePattern.Attachment;
import java.io.*;
public class MyWeeklyLog extends WeeklyLog {
protected Attachment attachment;
protected String name;
protected String date;
protected String content;
public Attachment getAttachment() {
return attachment;
}
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
//todo:shadowClone()方法实现浅拷贝
public CreationalPattern.PrototypePattern.AbstractPrototype.WeeklyLog shadowClone() {
Object obj = null;
try{
obj = super.clone();
return (CreationalPattern.PrototypePattern.AbstractPrototype.WeeklyLog)obj;
} catch (CloneNotSupportedException e) {
System.out.println("不支持复制");
return null;
}
}
//todo:deepClone()方法实现深拷贝
public WeeklyLog deepClone() throws IOException, ClassNotFoundException {
//todo:将对象写入流中
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
//todo:将对象从流中读出
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (WeeklyLog)ois.readObject();
}
}
五、附件类
package CreationalPattern.PrototypePattern;
import java.io.Serializable;
public class Attachment implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void download(){
System.out.println("下载附件 [ "+name+" ] ");
}
}
六、客户类
package CreationalPattern.PrototypePattern;
import CreationalPattern.PrototypePattern.AbstractPrototype.WeeklyLog;
import CreationalPattern.PrototypePattern.ConcretePrototype.MyWeeklyLog;
import java.io.IOException;
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//todo:浅拷贝
WeeklyLog preLog1,newLog1;
preLog1 = new MyWeeklyLog();
Attachment attachment1 = new Attachment();
preLog1.setAttachment(attachment1);
newLog1 = preLog1.shadowClone();
System.out.println("WeeklyLog是否相同?"+(preLog1 == newLog1));//false
System.out.println("Attachment是否相同?"+(preLog1.getAttachment() == newLog1.getAttachment()));//true
//todo:深拷贝
WeeklyLog preLog2,newLog2;
preLog2 = new MyWeeklyLog();
Attachment attachment2 = new Attachment();
preLog2.setAttachment(attachment2);
newLog2 = preLog2.deepClone();
System.out.println("WeeklyLog是否相同?"+(preLog2 == newLog2));//false
System.out.println("Attachment是否相同?"+(preLog2.getAttachment()==newLog2.getAttachment()));//false
}
}
七、测试结果
3 总结
3.1 原型模式优点
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新势力的创建效率。
- 扩展性较好,由于在原型模式中提高了创建原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统没有任何影响。
- 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式不需要,原型模式中产品的复制是通过封装在原型内部的克隆方法实现的,无需专门的工厂类。
- 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(例如恢复到某一历史状态),可辅助实现撤销操作。
3.2 原型模式缺点
- 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时需要修改源代码,违背了开闭原则。
- 在实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一次对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
3.3 原型模式适用环境
- 创建对象成本较大(例如初始化需要占用较长的时间、占用太多的CPU资源或者网络资源),新对象可以通过复制已有对象来获得,如果是相似对象,则可以对其成员变量稍作修改。
- 系统要保存对象的状态,而对象的状态变化很小。
- 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。