Java/Android 设计模式系列(11)--原型模式

  这篇博客我们来介绍一下剩下的最后一个创建型模式:原型模式(Prototype Pattern)。该模式有一个样板实例,用户从这个样板对象中复制出一个内部属性一致的对象,这个过程在 C++ 中就是一个克隆。被复制的实例就是我们所称的“原型”,这个原型是可定制的。原型模式多用于创建复杂的或者构造耗时的实例,因为这种情况下,复制一个已经存在的实例可以使程序运行效率更高。
  这个模式的重点在于,客户端的代码在不知道要实例化何种特定类的情况下,就可以制造出新的实例。

设计模式总目录

  Java/Android 设计模式系列–目录

特点

  用原型实例指向创建对象的种类,并通过拷贝这些原型创建新的对象。
  原型模式的适用场景:

  • 类初始化需要消耗很多的资源,这个资源包括数据,硬件资源等,简而言之,这个实例化的过程很昂贵时,就可以使用原型模式;
  • 频繁的创建相似的对象时,比如需要在一个循环体内创建对象,假如对象创建过程比较复杂或者循环次数很多的话,使用原型模式不但可以简化创建过程,而且可以使系统的整体性能提高很多;
  • 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,这时可以使用原型模式;
  • 一个对象需要提供给其他对象访问,并且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用,即保护性拷贝;
  • 避免在客户端调用一个对象子类的生成器,即和抽象工厂模式一样,子类的生成器对客户端不透明。

      需要注意的,通过实现 Cloneable 接口的原型模式在调用 clone 函数构造实例并不一定比通过 new 操作速度快,只有通过 new 构造对象较为耗时或者说成本较高时,通过 clone 方法才能够获得效率上的提升。因此,在使用 Cloneable 时需要考虑构建对象的成本以及做一些效率上的测试,当然,实现原型模式也不一定非要实现 Cloneable 接口,也有其他的实现方式。

UML类图

  我们来看看原型模式的 uml 类图:
  这里写图片描述
原型模式有三个角色:

  • Client:客户端用户;
  • Prototype:抽象类或者接口,声明具备 clone 的能力;
  • ConcretePrototype:具体的原型类。

为了实现原型模式,首先要声明一个虚基类或者接口,该类中有一个单纯的虚方法 clone,任何一个需要多态构造函数性质的类都需要继承自该虚类或者实现该接口,并且实现 clone 方法;然后客户端不直接调用类的构造函数,而是调用原型类的 clone 方法用来生成另一个对象,或者使用其他设计模式提供的功能来调用 clone 方法,比如工厂模式等。
  据此我们就可以写出原型模式的通用代码:
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/Jakey-jp/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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值