原型模式
原型模式
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
介绍
意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
主要解决:在运行期建立和删除原型。
何时使用: 1、当一个系统应该独立于它的产品创建,构成和表示时。 2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。 3、为了避免创建一个与产品类层次平行的工厂类层次时。 4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
如何解决:利用已有的一个原型对象,快速地生成和原型对象一样的实例。
关键代码: 1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。 2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。
应用实例: 1、细胞分裂。 2、JAVA 中的 Object clone() 方法。
优点: 1、性能提高。 2、逃避构造函数的约束。
缺点: 1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。 2、必须实现 Cloneable 接口。
使用场景: 1、资源优化场景。 2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。 3、性能和安全要求的场景。 4、通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。 5、一个对象多个修改者的场景。 6、一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。 7、在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。
注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。
角色
-
Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
-
ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
-
Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
原型模式的核心在于如何实现克隆方法。
原型模式示例
Java语言提供的clone()方法
学过Java语言的人都知道,所有的Java类都继承自 java.lang.Object
。事实上,Object
类提供一个 clone()
方法,可以将一个Java对象复制一份。因此在Java中可以直接使用 Object
提供的 clone()
方法来
实现对象的克隆,Java语言中的原型模式实现很简单。
需要注意的是能够实现克隆的Java类必须实现一个 标识接口 Cloneable
,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一
个 CloneNotSupportedException
异常。
定义一个抽象原型角色,抽象类,实现Cloneable接口:
public abstract class Prototype implements Cloneable { public Prototype clone() { Prototype prototype = null; try { prototype = (Prototype)super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return prototype; } public abstract void show(); }
定义一个具体原型角色,继承Prototype类:
public class ConcretePrototype extends Prototype { public void show() { System.out.println("ConcretePrototype.show()"); } }
定义一个客户端调用:
public class Client { public static void main(String[] args) { ConcretePrototype cp = new ConcretePrototype(); for (int i = 0; i < 10; i++) { ConcretePrototype clonecp = (ConcretePrototype)cp.clone(); clonecp.show(); } } }
克隆对象的内存地址均不同,说明不是同一个对象,克隆成功,克隆仅仅通过调用 super.clone()
即可。
比方说一个类实例很有用的时候,就可以使用原型模式去复制它。不过原型模式单独用得不多,一般是和其他设计模式一起使用。
浅克隆与深克隆
看下面的示例
1 public class Pig implements Cloneable{ 2 private String name; 3 private Date birthday; 4 // ...getter, setter, construct 5 @Override 6 protected Object clone() throws CloneNotSupportedException { 7 Pig pig = (Pig)super.clone(); 8 return pig; 9 } 10 @Override 11 public String toString() { 12 return "Pig{" + 13 "name='" + name + '\'' + 14 ", birthday=" + birthday + 15 '}'+super.toString(); 16 } 17 }
测试
public class Test { public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { Date birthday = new Date(0L); Pig pig1 = new Pig("佩奇",birthday); Pig pig2 = (Pig) pig1.clone(); System.out.println(pig1); System.out.println(pig2); pig1.getBirthday().setTime(666666666666L); System.out.println(pig1); System.out.println(pig2); } }
输出结果:
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@27973e9b Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@312b1dae Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@27973e9b Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@312b1dae
我们可以发现:
-
pig1
与pig2
的内存地址不同 -
对
pig1
设置了时间,同事pig2
的时间也改变了
这里引出浅拷贝与深拷贝。
在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。
浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。
浅克隆:
-
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
-
简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
-
在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。
深克隆:
-
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
-
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
-
在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。
实现深克隆
方式一,手动对引用对象进行克隆:
@Override protected Object clone() throws CloneNotSupportedException { Pig pig = (Pig)super.clone(); //深克隆 pig.birthday = (Date) pig.birthday.clone(); return pig; }
方式二,通过序列化的方式:
public class Pig implements Serializable { private String name; private Date birthday; // ...省略 getter, setter等 protected Object deepClone() throws CloneNotSupportedException, IOException, ClassNotFoundException { //将对象写入流中 ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(this); //将对象从流中取出 ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return (ois.readObject()); } }
破坏单例模式
1 public class HungrySingleton implements Serializable, Cloneable { 2 3 private final static HungrySingleton hungrySingleton; 4 5 static { 6 hungrySingleton = new HungrySingleton(); 7 } 8 private HungrySingleton() { 9 if (hungrySingleton != null) { 10 throw new RuntimeException("单例构造器禁止反射调用"); 11 } 12 } 13 public static HungrySingleton getInstance() { 14 return hungrySingleton; 15 } 16 private Object readResolve() { 17 return hungrySingleton; 18 } 19 @Override 20 protected Object clone() throws CloneNotSupportedException { 21 return super.clone(); 22 } 23 }
使用反射获取对象,测试如下
1 public class Test { 2 public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { 3 HungrySingleton hungrySingleton = HungrySingleton.getInstance(); 4 Method method = hungrySingleton.getClass().getDeclaredMethod("clone"); 5 method.setAccessible(true); 6 HungrySingleton cloneHungrySingleton = (HungrySingleton) method.invoke(hungrySingleton); 7 System.out.println(hungrySingleton); 8 System.out.println(cloneHungrySingleton); 9 } 10 }
输出
1 com.designpattern.HungrySingleton@34c45dca 2 com.designpattern.HungrySingleton@52cc8049
可以看到,通过原型模式,我们把单例模式给破坏了,现在有两个对象了
为了防止单例模式被破坏,我们可以:不实现 Cloneable
接口;或者把 clone
方法改为如下
1 @Override 2 protected Object clone() throws CloneNotSupportedException { 3 return getInstance(); 4 }
原型模式在Java中的应用及解读
既然原型模式的关注点是在于通过克隆自身来获取一个和自身一样的对象,那其实只要是实现了Cloneable接口的类都可以算是原型模式的应用,比如ArrayList吧:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { ... public Object clone() { try { ArrayList<E> v = (ArrayList<E>) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(); } } ... }
程序中获取到了一个ArrayList的实例arrayList,我们完全可以通过调用arrayList.clone()方法获取到原ArrayList的拷贝。
同理,我们看看 HashMap
1 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { 2 @Override 3 public Object clone() { 4 HashMap<K,V> result; 5 try { 6 result = (HashMap<K,V>)super.clone(); 7 } catch (CloneNotSupportedException e) { 8 // this shouldn't happen, since we are Cloneable 9 throw new InternalError(e); 10 } 11 result.reinitialize(); 12 result.putMapEntries(this, false); 13 return result; 14 } 15 // ...省略... 16 }
原型模式总结
原型模式的主要优点如下:
-
当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
-
扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
-
原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
-
可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
原型模式的主要缺点如下:
-
需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
-
在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
适用场景:
-
创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
-
如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
-
需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。