详解原型模式
引言
在某些情况下,有些对象的创建过程较为复杂,而且有时候需要频繁创建,因此使用常规的创建方法就可能导致性能下降。原型模式通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象,这就是原型模式存在的意义。
1.概念
原型模式(Prototype Pattern):原型模式是一种对象创建型模式,用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。原型模式允许一个对象再创建另外一个可定制的对象,无须知道任何创建的细节。
2.模式结构图
3.模式分析
Prototype:抽象原型类,声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
ConcretePrototype:具体原型类,实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。核心代码如下:
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;
}
}
Client:客户类,让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。核心代码如下:
Prototype obj1 = new concretePrototype();
obj1.setAttr("hello");
Prototype obj2 = obj1.clone();
4.具体实例分析
上述具体原型类的核心是clone()方法的实现,是一种通用的实现思路,与编程语言无关。在Java中,所有的类都继承java.lang.Object父类,Object类提供clone()方法供对象克隆来调用。但是涉及到拷贝问题,就需要讨论浅拷贝和深拷贝。
4.1.浅拷贝
浅拷贝,拷贝:值类型的值和引用类型的引用地址。对于值类型的变量,复制一份值,修改克隆变量的值不会影响原变量的值;对于引用类型的变量,复制引用的地址,修改克隆引用指向的对象会影响原引用指向的对象,因为它们指向同一个对象。
Point:图形坐标类,具体代码如下:
public class Point{
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
Shape:具体原型类,使用Object的clone()方法必须实现Cloneable接口,然后重写clone()方法。(Object类是抽象原型类)
public class Shape implements Cloneable{
private int area;
private String type;
private Point point;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getArea() {
return area;
}
public void setArea(int area) {
this.area = area;
}
public Point getPoint() {
return point;
}
public void setPoint(Point point) {
this.point = point;
}
@Override
public Shape clone(){
try{
return (Shape) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("不支持拷贝");
return null;
}
}
}
Client:客户端,通过创建一个对象,然后调用这个对象的clone()方法克隆一个新对象,比较这两个对象。
public class Client {
public static void main(String[] args) {
Shape shape1 = new Shape();
shape1.setType("圆形");
shape1.setArea(100);
shape1.setPoint(new Point(2,5));
Shape shape2 = shape1.clone();
System.out.println("shape1==shape2? " + (shape1==shape2));
System.out.println("shape1.Point==shape2.Point? " + (shape1.getPoint()==shape2.getPoint()));
}
}
运行代码发现,clone()方法是浅拷贝,拷贝了一个新对象,但是对于引用类型仅拷贝了引用的地址,因此第二行输出为true。要想实现深拷贝,有多种方法,这里介绍递归调用clone()的方式。
4.2.深拷贝
深拷贝:拷贝值和引用的对象,也就是说如果是引用类型,那么拷贝的是一个独立的对象,修改克隆对象中引用类型的对象,不会影响原对象的引用类型变量。
修改引用类型Point的类,也实现Cloneable接口,并重写clone()方法。具体实例代码如下:
public class Point implements Cloneable{
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
@Override
public Point clone(){
try{
return (Point)super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("不支持拷贝");
return null;
}
}
}
修改Shape类的代码,先使用super.clone()方法创建当前对象的克隆,然后修改克隆中引用类型的属性,让其指向引用类型的克隆。具体实例如下:
public class Shape implements Cloneable{
private int area;
private String type;
private Point point;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getArea() {
return area;
}
public void setArea(int area) {
this.area = area;
}
public Point getPoint() {
return point;
}
public void setPoint(Point point) {
this.point = point;
}
@Override
public Shape clone(){
try{
Shape clonedShape = (Shape) super.clone();
clonedShape.point = point.clone();
return clonedShape;
} catch (CloneNotSupportedException e) {
System.out.println("不支持拷贝");
return null;
}
}
}
注意:如果出现层层调用的类,那么要实现深拷贝,就需要为每一个类都实现Cloneable接口,通俗来讲就是:你现在拷贝的对象(克隆对象1)引用类型的值(引用对象1)是引用的地址对吧,没问题,我拷贝一个引用类型的新对象(引用对象1的克隆对象),这个新对象和引用地址的对象不是同一个对象,你现在指向这个新对象,此时这个克隆对象1就完成了深拷贝。
运行代码如下:
可以发现,此时已经是深拷贝,引用类型的值和原对象的引用类型的值指向的不是同一个对象。
5.优缺点
主要优点如下:
(1)当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
(2)扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
(3)原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
(4)可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
主要缺点如下:
(1)需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
(2)在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
6.适用场景
(1)创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
(2)如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
(3)需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。