设计模式(五):利用原型模式复制几个葫芦娃

时间:2022-07-25
本文章向大家介绍设计模式(五):利用原型模式复制几个葫芦娃,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前面的文章介绍了工厂模式和抽象工厂模式、建造者模式,它们都属于创建型设计模式,今天介绍另外一种创建性设计模式,原型模式(prototype pattern)。

什么时候用原型模式?

我个人一直有种观点,就是骨子里懒惰的程序员都喜欢用设计模式,因为懒所以得想尽办法让自己更高效。

原型模式就是一种懒操作,用于频繁创建对象的场所。

日常生活中,我们都说两个人很像,简直就是一个模子里引出来的。

那么,代码中原型模式就是这样基于一个对象快速复制另外一个对象,而无需重头全新创建。

比如,代码要产生 7 个葫芦娃,怎么用原型模式做呢?

葫芦娃长这样:

每个葫芦娃长得都差不多,可能就名字不同,衣服颜色不同。

我们可以定义一个葫芦娃类,然后先创建第一个,然后通过原型模式快速复制更多个。

原型模式实现

非常简单,先定义一个 Cloneable 接口,然后定义一个实现类 CalabashBrother。

因为 Java 语言就支持 clone 操作,所以很容易写出原型设计模式代码。

Java 例子

public class CalabashBrother implements  Cloneable{
    
    String name;
    String color;

    public CalabashBrother(String name,String color){
        System.out.println("---Construct obj---");
        this.name = name;
        this.color = color;
    }

    @Override
    public String toString(){
        return "ID:"+hashCode()+" My name is "+this.name+" my skin is " + this.color;
    }

    @Override
    protected Object clone(){
        CalabashBrother brother = null;

        try {
            brother = (CalabashBrother) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }

        return brother;
    }
}

定义了 CalabashBrother 代表葫芦娃。

复写了 Clone 方法。

另外复写了 toString() 方法,方便调试时查看对象信息,这里用 hashcode 指示不同的对象。

然后可以写 Demo 测试了。

public class PrototypeDemo {

    public static void main(String[] args) {
        
        String colorlist[] = {"red","green","yellow","crown","pink","blue","purple"};

        CalabashBrother brother1 = new CalabashBrother("brother1",colorlist[0]);
        System.out.println(brother1);

        for (int i = 1;i < 7;i++) {
            CalabashBrother brother = (CalabashBrother)brother1.clone();
            brother.name = "brother"+(i+1);
            brother.color = colorlist[i];
            System.out.println(brother);
        }

    }
}

运行程序,结果如下:

---Construct obj---
ID:2129789493 My name is brother1 my skin is red
ID:668386784 My name is brother2 my skin is green
ID:1329552164 My name is brother3 my skin is yellow
ID:363771819 My name is brother4 my skin is crown
ID:2065951873 My name is brother5 my skin is pink
ID:1791741888 My name is brother6 my skin is blue
ID:1595428806 My name is brother7 my skin is purple

大家仔细观看输出可以发现 2 个特征:

  1. 只调用了一次构造函数,后面通过 clone() 的对象没有再调用类的构造函数。
  2. clone 出来的对象 ID 不一样,这样保证了是新的的对象。

浅拷贝和深拷贝

讲原型模式的时候离不开这个话题。

浅拷贝的时候只考虑值传递,如果一个类中有引用其它对象,那么只会复制对象的引用。

深拷贝除了值传递外,引用的内容同样也会复制。

public class CalabashBrother implements  Cloneable{
    
    String name;
    String color;
    
    Calabash calabash = new Calabash();
    
    public CalabashBrother(String name,String color){
        System.out.println("---Construct obj---");
        this.name = name;
        this.color = color;
    }
}

假设 CalabashBrother 中有个对象是 calabash,如果直接调用 super.clone() 假设 clone 出来的对象是 obj,那么 obj.calabash 其实和原型对象是同一份内容。

这就是**浅拷贝**。

如果要实现**深拷贝**也很简单,只用一个个对象手动调用 clone() 就好了。

```java
@Override
protected Object clone(){
    CalabashBrother brother = null;

    try {
        brother = (CalabashBrother) super.clone();
        brother.calabash = (Calabash) calabash.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }

    return brother;
}

Android 源码中的原型模式

Android 源码中的原型模式多不胜数,这里摘抄一段。

package android.animation;


public abstract class Keyframe implements Cloneable {

    boolean mHasValue;


    private TimeInterpolator mInterpolator = null;



    @Override
    public abstract Keyframe clone();

    /**
     * This internal subclass is used for all types which are not int or float.
     */
    static class ObjectKeyframe extends Keyframe {

      
        Object mValue;

        ObjectKeyframe(float fraction, Object value) {
            mFraction = fraction;
            mValue = value;
            mHasValue = (value != null);
            mValueType = mHasValue ? value.getClass() : Object.class;
        }


        @Override
        public ObjectKeyframe clone() {
            ObjectKeyframe kfClone = new ObjectKeyframe(getFraction(), hasValue() ? mValue : null);
            kfClone.mValueWasSetOnStart = mValueWasSetOnStart;
            kfClone.setInterpolator(getInterpolator());
            return kfClone;
        }
    }


}

Keyframe 是 Android 动画中的关键帧,我们都知道动画是连续的一帧帧图片指定时间播放形成的视觉效果,Android 中的 SDK 有很方便的动画定义 API,一段动画中有许多关键帧,关键帧之间通过数学插值的方法自动生成其它帧,因为类型一样,所以关键帧可以通过原型模式来实现。

总结

原型模式非常有用,对于需要频繁创建对象的场景,用原型模式再合适不过了,关键原因在于拷贝时是直接从内存中获取而不是重新再执行一次构造函数。

实际上,不单单是 Java 语言会用到这个模式,其它开发语言只要是频繁创建同类型不同对象都可以用到,比如,我做 OpenCV 的时候,处理视频流的时候就经常要对 mat 进行原型拷贝。