这篇博客我们来介绍一下剩下的最后一个创建型模式:原型模式(Prototype Pattern)。该模式有一个样板实例,用户从这个样板对象中复制出一个内部属性一致的对象,这个过程在 C++ 中就是一个克隆。被复制的实例就是我们所称的“原型”,这个原型是可定制的。原型模式多用于创建复杂的或者构造耗时的实例,因为这种情况下,复制一个已经存在的实例可以使程序运行效率更高。
这个模式的重点在于,客户端的代码在不知道要实例化何种特定类的情况下,就可以制造出新的实例。
转载请注明出处:http://blog.csdn.net/self_study/article/details/51757525。
PS:对技术感兴趣的同鞋加群544645972一起交流。
设计模式总目录
特点
用原型实例指向创建对象的种类,并通过拷贝这些原型创建新的对象。
原型模式的适用场景:
- 类初始化需要消耗很多的资源,这个资源包括数据,硬件资源等,简而言之,这个实例化的过程很昂贵时,就可以使用原型模式;
- 频繁的创建相似的对象时,比如需要在一个循环体内创建对象,假如对象创建过程比较复杂或者循环次数很多的话,使用原型模式不但可以简化创建过程,而且可以使系统的整体性能提高很多;
- 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,这时可以使用原型模式;
- 一个对象需要提供给其他对象访问,并且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用,即保护性拷贝;
- 避免在客户端调用一个对象子类的生成器,即和抽象工厂模式一样,子类的生成器对客户端不透明。
UML类图
我们来看看原型模式的 uml 类图:
原型模式有三个角色:
- Client:客户端用户;
- Prototype:抽象类或者接口,声明具备 clone 的能力;
- ConcretePrototype:具体的原型类。
据此我们就可以写出原型模式的通用代码:
ConcretePrototype.class
public class ConcretePrototype implements Cloneable{
private String string;
private ArrayList<String> stringList;
public ConcretePrototype() {
stringList = new ArrayList<>();
}
public String getString() {
return string;
}
public void setString(String string) {
this.string = string;
}
public ArrayList<String> getStringList() {
return stringList;
}
public void setStringList(ArrayList<String> stringList) {
this.stringList = stringList;
}
public ConcretePrototype clone() {
try {
ConcretePrototype copy = (ConcretePrototype) super.clone();
copy.setString(this.getString());
copy.setStringList((ArrayList<String>) getStringList().clone());
return copy;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
我们直接使用系统的 Cloneable 接口来作为 Prototype 角色,接着重写Object类中的clone方法。Java 中,所有类的父类都是 Object 类,Object 类中有一个 clone 方法,作用是返回对象的一个拷贝,但是其作用域 protected 类型的,一般的类无法调用,因此,Prototype 类需要将 clone 方法的作用域修改为 public 类型。测试代码:
ConcretePrototype src = new ConcretePrototype();
src.setString("src");
src.getStringList().add("src 1");
src.getStringList().add("src 2");
ConcretePrototype des = src.clone();
des.setString("des");
des.getStringList().add("des 1");
des.getStringList().add("des 2");
Log.e("shawn", "src.string = " + src.getString() +" des.string = " + des.getString());
StringBuilder builder = new StringBuilder();
for (String temp : src.getStringList()) {
builder.append(temp).append(" ");
}
Log.e("shawn", "src.stringList = " + builder.toString());
builder = new StringBuilder();
for (String temp : des.getStringList()) {
builder.append(temp).append(" ");
}
Log.e("shawn", "des.stringList = " + builder.toString());
结果:
com.android.prototypepattern E/shawn: src.string = src des.string = des
com.android.prototypepattern E/shawn: src.stringList = src 1 src 2
com.android.prototypepattern E/shawn: des.stringList = src 1 src 2 des 1 des 2
上面的代码很简单,使用了 Cloneable 接口,但是其实 Cloneable 接口就是一个空类:
/**
* This (empty) interface must be implemented by all classes that wish to
* support cloning. The implementation of {@code clone()} in {@code Object}
* checks if the object being cloned implements this interface and throws
* {@code CloneNotSupportedException} if it does not.
*
* @see Object#clone
* @see CloneNotSupportedException
*/
public interface Cloneable {
// Marker interface
}
注释很清楚了,是用来赋予一个类的 clone 功能的,继续看看 Object 类的 clone 函数:
protected Object clone() throws CloneNotSupportedException {
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class " + getClass().getName() +
" doesn't implement Cloneable");
}
return internalClone();
}
这也是为什么需要继承 Cloneable 接口的原因,不继承该接口这里就会直接抛出 CloneNotSupportedException 异常,internalClone 是一个 native 函数:
private native Object internalClone();
它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。
这里需要注意的是浅拷贝(影子拷贝)与深拷贝的问题,学过 C++ 对拷贝都应该印象很深,在上面的例子中用的是一个 String 和一个 ArrayList 的对象,对于这两个对象,clone 函数里的拷贝写法是不一样的,一个是直接 set ,另一个需要继续调用 ArrayList 的 clone 方法生成一个 ArrayList 的拷贝对象 set 进 copy 对象中,如果直接将源对象中的 ArrayList 对象 set 进 copy 对象中,就会造成客户端获取到 copy 对象之后,可以通过 copy 对象修改源对象的数据,这在保护原型模式中是绝对不允许的,所以这里不能使用浅拷贝,必须要使用深拷贝。Object 类的 clone 方法只会拷贝对象中的 8 种基本数据类型 ,byte 、char、short、int、long、float、double、boolean,对于数组、容器对象、引用对象等都不会拷贝,这就是浅拷贝,如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝,如上面的 ArrayList 一样。String 这个类型必须要单独拿出来说一下,这个类型实际上算是一个浅拷贝,因为 src 与 拷贝后的 copy 对象指向的是同一个内存区域,但是由于 Java 中 String 的特殊不可变性(除去反射之外,不能修改一个 String 对象所指向的字符串内容),具体可以参考这篇博客:Java中的String为什么是不可变的? – String源码分析,所以从实际表现效果来看,String 和 8 种基础类型一样,可以认为是深拷贝。
示例与源码
原型模式的代码结构很简单,我们这就以 Android 中的 Intent 类为例,来简单了解一下 Intent 类的原型模式:
Intent.class
@Override
public Object clone() {
return new Intent(this);
}
...
/**
* Copy constructor.
*/
public Intent(Intent o) {
this.mAction = o.mAction;
this.mData = o.mData;
this.mType = o.mType;
this.mPackage = o.mPackage;
this.mComponent = o.mComponent;
this.mFlags = o.mFlags;
this.mContentUserHint = o.mContentUserHint;
if (o.mCategories != null) {
this.mCategories = new ArraySet<String>(o.mCategories);
}
if (o.mExtras != null) {
this.mExtras = new Bundle(o.mExtras);
}
if (o.mSourceBounds != null) {
this.mSourceBounds = new Rect(o.mSourceBounds);
}
if (o.mSelector != null) {
this.mSelector = new Intent(o.mSelector);
}
if (o.mClipData != null) {
this.mClipData = new ClipData(o.mClipData);
}
}
和我们平时使用的 clone 函数不一样,Intent 类并没有调用 super.clone ,而是专门写了一个拷贝构造函数用来克隆对象,我们在上文提到过,使用 clone 和 new 需要根据构造函数的成本来决定,如果对象的构造成本比较高或者构造较为麻烦,那么使用 clone 函数效率较高,否则可以使用 new 的形式。这就和 C++ 中的拷贝构造函数完全一致,将原始对象作为构造函数的参数,然后在构造函数中奖原始对象的数据逐个拷贝一遍,这样,整个克隆过程就完成了。
总结
原型模式是非常简单的一个设计模式,它的核心问题就是对原始对象进行拷贝,在这个模式的使用过程中需要注意的一点就是:深、浅拷贝的问题。在开发过程中,为了减少错误,应该尽量使用深拷贝,避免操作副本时影响原始对象的问题。
同时需要特别注意的是使用原型模式复制对象不会调用类的构造方法。因为对象的复制是通过调用 Object 类的 clone 方法来完成的,它直接在内存中复制数据,因此不会调用到类的构造方法。不但构造方法中的代码不会执行,甚至连访问权限都对原型模式无效。
原型模式的优点和缺点基本也就明了了:
- 优点
- 原型模式是在内存中二进制流的拷贝,要比直接 new 一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好的体现其优点,而且可以向客户端隐藏制造新实例的复杂性;
- 缺点
- 直接在内存中拷贝,构造函数是不会执行的,在实际开发中应该注意这个潜在的问题,另外,对象的复制有时候相当复杂,特别需要注意的是不彻底深拷贝的问题。
创建型模式 Rules of thumb
有些时候创建型模式是可以重叠使用的,有一些抽象工厂模式和原型模式都可以使用的场景,这个时候使用任一设计模式都是合理的;在其他情况下,他们各自作为彼此的补充:抽象工厂模式可能会使用一些原型类来克隆并且返回产品对象。
抽象工厂模式,建造者模式和原型模式都能使用单例模式来实现他们自己;抽象工厂模式经常也是通过工厂方法模式实现的,但是他们都能够使用原型模式来实现;
通常情况下,设计模式刚开始会使用工厂方法模式(结构清晰,更容易定制化,子类的数量爆炸),如果设计者发现需要更多的灵活性时,就会慢慢地发展为抽象工厂模式,原型模式或者建造者模式(结构更加复杂,使用灵活);
原型模式并不一定需要继承,但是它确实需要一个初始化的操作,工厂方法模式一定需要继承,但是不一定需要初始化操作;
使用装饰者模式或者组合模式的情况通常也可以使用原型模式来获得益处;
单例模式中,只要将构造方法的访问权限设置为 private 型,就可以实现单例。但是原型模式的 clone 方法直接无视构造方法的权限来生成新的对象,所以,单例模式与原型模式是冲突的,在使用时要特别注意。
源码下载
https://github.com/zhaozepeng/Design-Patterns/tree/master/PrototypePattern
引用
https://en.wikipedia.org/wiki/Prototype_pattern
http://blog.csdn.net/jason0539/article/details/23158081
http://blog.csdn.net/zhangjg_blog/article/details/18369201
http://blog.csdn.net/zhangjg_blog/article/details/18319521