原型模式(Prototype Pattern),用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象;即用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
在运行期建立和删除原型;利用一个已有的原型对象,快速的生成和原型对象一样的实例;
使用场景:
1、当一个系统应该独立于它的产品创建,构成和表示时。
2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。
3、为了避免创建一个与产品类层次平行的工厂类层次时。
4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
实例解析:
原型类以及具体原型实现类的代码在这里就不在列出了,详情请看上面的类图;
创建一个类,从数据库获取实体类,并把它们存储在一个 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);
//返回一个shapeId对应的Shape的克隆对象,注意Shape类要实现Cloneable接口
return (Shape) cachedShape.clone();
}
// 简化操作:不查数据库了,直接后台添加三种形状
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);
}
}
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());
}
}
输出结果:
Shape : Circle
Shape : Square
Shape : Rectangle
实现的关键:
1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone() 方法来实现对象的浅复制或通过序列化(serializable)的方式来实现深复制。
2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些”易变类”拥有稳定的接口。
原型模式的优点:
通过普通的new方式实例化对象时,每new一次就需要执行一次构造函数,如果构造函数的执行时间很长,那么多次执行程序效率就会大打折扣;一般在初始化的信息不发生变化的情况下,克隆是最好的办法,这既隐藏了对象的创建细节,又大大提升了性能。
浅复制 VS 深复制
以大话设计模式里面的一个经典的简历的例子来学习这两个概念:
浅复制
WorkExperience.java
public class WorkExperience{
private string workDate;
private string company;
public string workDate(){
//getter and setter
}
public string company(){
//getter and setter
}
}
Resume.java
public class Resume implements ICloneable{
private string name;
private string sex;
private string age;
private WorkExperience work;//引用工作经历对象
//构造函数
public Resume(string name){
this.name = name;
work = new WorkExperience();
}
//设置个人信息
public void setPersonalInfo(String sex,String age){
this.sex = sex;
this.age = age;
}
//设置工作经历
public void setWorkExperience(String timeArea,String company){
work.timeArea = timeArea;
work.company = company;
}
//显示
public void display(){
System.out.println("姓名:" + name + " 性别:" + sex + " 年龄:" + age);”
System.out.println("工作经历:" + work.timeArea +" " + company);
}
//克隆方法
public Object Clone(){
return (Object)this.MemberwiseClone();
}
}
测试类:
public class TestPrototype{
public satic void main(String[] args){
Resume resumeOne = new Resume( "tom");
resumeOne.SetPersonalInfo( "man","22");
resumeOne.SetWorkExperience( "2015-1-2至2015-12-10","IBM");
Resume resumeTwo = (Resume)resumeOne.Clone();
resumeTwo.SetWorkExperience( "2015-1-2至2015-11-11","Microsoft");
Resume resumeThree = (Resume)resumeOne.Clone();
resumeThree.SetPersonalInfo( "man", "32");
resumeThree.SetWorkExperience( "2015-1-2至2015-11-11","甲骨文");
resumeOne.Display();
resumeTwo.Display();
resumeThree.Display();
Console.Read();
}
}
在你的想象里输出结果会是什么呢?我们拿事实来说话:
姓名:tom 性别:man 年龄:22
工作经历:2015-1-2至2015-11-11 甲骨文
姓名:tom 性别:man 年龄:22
工作经历:2015-1-2至2015-11-11 甲骨文
姓名:tom 性别:man 年龄:32
工作经历:2015-1-2至2015-11-11 甲骨文
看到结果有没有很惊讶,对于工作经历的输出相信出乎了好多人的意料,这个地方我们就要说到MemberwiseClone()这个方法了,(有关MemberwiseClone的详细内容请参见本人的另一篇文章
),其对于值类型没有问题,但是对于引用类型,就只是复制了引用,对引用对象还是指向了原来的对象,所以就会出现我们给resume one two three三处设置工作经历,但却得到三个引用都是最后一次的设置,这是因为三个引用都指向了同一对象。
那么问题来了,到底什么是浅复制呢?
浅复制:被复制对象的所有变量都含有与原来对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。
然而我们希望实现的是一变二,二变三,我么叫这种方式为深复制:把引用对象的变量指向复制过的新对象,而不是原来的被引用的对象。(使用深复制要注意避免循环引用的问题)
深复制如何实现这个例子呢?简历类和工作经历类都实现了ICloneable接口;
WorkExperience.java
public class WorkExperience implements ICloneable{
private string workDate;
private string company;
public string workDate(){
//getter and setter
}
public string company(){
//getter and setter
}
//克隆方法
public Object Clone(){
return (Object)this.MemberwiseClone();
}
}
Resume .java
public class Resume implements ICloneable{
private string name;
private string sex;
private string age;
private WorkExperience work;//引用工作经历对象
//构造函数
public Resume(string name){
this.name = name;
work = new WorkExperience();
}
//注意这个构造方法
private Resume(WorkExperience work){
this.work = work.(WorkExperience)clone;
}
//设置个人信息
public void setPersonalInfo(String sex,String age){
this.sex = sex;
this.age = age;
}
//设置工作经历
public void setWorkExperience(String timeArea,String company){
work.timeArea = timeArea;
work.company = company;
}
//显示
public void display(){
System.out.println("姓名:" + name + "性别:" + sex + "年龄:" + age);
System.out.println("工作经历:" + work.timeArea + company);
}
//注意区别该类实现的克隆方法与浅复制时实现的克隆方法的区别
public Object Clone(){
//调用私有的构造方法克隆工作经历,然后再给建立对象赋值
Resume resume = new Resume(this.work);
resume.name = this.name;
resume.sex = this.sex;
resume.age = this.age;
}
}
测试类同上,这次输出结果会是什么呢?
姓名:tom 性别:man 年龄:22
工作经历:2015-1-2至2015-12-10 IBM
姓名:tom 性别:man 年龄:22
工作经历:2015-1-2至2015-11-11 Microsoft
姓名:tom 性别:man 年龄:32
工作经历:2015-1-2至2015-11-11 甲骨文
由于在一些特定场合,会经常涉及深复制和浅复制,比如说,数据集对象DataSet,他就有Clone()和copy()方法,Clone()方法用来复制DataSet的结构但不复制其数据,实现了原型模式中的浅复制;而copy()方法不但复制其结构还复制其数据,也就是实现了原型模式中的深复制。