设计模式 ( 三 ) 原型模式

介绍

原型模式是一个建立型的模式。原型二字代表了该模式应该有一个模板实例,用户从这个模板对象中复制出一个内部属性一致而且内存地址不一样的对象,这个过程也就是咱们俗称的 "克隆" 。被复制的实例就是咱们所称的 “原型” ,这个原型是可定制的。原型模式多用于建立复杂的或者构造耗时的实例,由于这种状况下,复制一个已经存在的实例可以使程序运行更高效。html

定义

用原型实例指定建立对象的种类,并经过复制这些原型建立新的对象。java

使用场景

  1. 类初始化须要消耗很是多的资源,这个资源包括数据、硬件资源等,经过原型复制避免这些消耗。
  2. 经过 new 产生一个对象须要很是繁琐的数据准备和访问权限,这时可使用原型模式。
  3. 一个对象须要提供给其它对象访问,并且各个调用者可能都须要修改其值时,能够考虑使用原型模式复制多个对象供调用者使用,既保护性拷贝。

UML 类图

cGAVM.png

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

原型模式的简单实现

下面以简单的文档 copy 为例来演示一下原型模式。android

需求:有一个文档,文档中包含了文字和图片,用户通过了长时间的内容编辑后,打算对该文档作进一步的编辑,可是,这个编辑后的文档是否会被采用还不肯定,所以,为了安全起见,用户须要将当前文档 copy 一份,而后再在文档副本上进行修改。git

/***这里表明是具体原型类*/
public class WordDocument implements Cloneable {

    /** * 文本 */
    private String mTxt;

    /** * 图片名列表 */
    private List<String> mImagePath = new ArrayList<>();


    public String getmTxt() {
        return mTxt;
    }

    public void setmTxt(String mTxt) {
        this.mTxt = mTxt;
    }

    public List<String> getImagePath() {
        return mImagePath;
    }

    public void addImagepath(String imagepath) {
        mImagePath.add(imagepath);
    }

    /** * 打印文档内容 */
    public void println(){

        System.out.println("---------------- start ----------------");
        
        System.out.println("txt: " + mTxt);
        System.out.println("mImagePath: ");
        for (String path : mImagePath) {
            System.out.println("path: " + path);
        }

        System.out.println("----------------- end ----------------");
    }

    /** * 声明具有 clone 能力 * @return clone 的对象 */
    @Override
    protected WordDocument clone() {
        try {
            WordDocument document = (WordDocument)super.clone();
            document.mTxt = this.mTxt;
            document.mImagePath = this.mImagePath;
            return document;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
}
复制代码

Test:github

@Test
    public void test4(){
        //1. 构建文档对象
        WordDocument wordDocument = new WordDocument();
        //2. 编辑文档
        wordDocument.setmTxt("今天是一个好天气");
        wordDocument.addImagepath("/sdcard/image.png");
        wordDocument.addImagepath("/sdcard/image2.png");
        wordDocument.addImagepath("/sdcard/image3.png");
        //打印文档内容
        wordDocument.println();


        System.out.println("--------------------开始clone-----\n\n");

        //以原始文档为准,copy 副本
        WordDocument cloneDoc = wordDocument.clone();

        System.out.println(" 打印副本,看看数据 \n\n");
        //打印副本,看看数据
        cloneDoc.println();

        //在副本文档上修改
        cloneDoc.setmTxt("副奔上修改文档:老龙王哭了");
        System.out.println(" 打印修改后的副本 \n\n");
        //打印修改后的副本
        cloneDoc.println();
        System.out.println("----看会不会影响原始文档-----\n\n");
        //看会不会影响原始文档???????
        wordDocument.println();
      System.out.println("内存地址:\nwordDocument: "+wordDocument.toString() +"\n" + "cloneDoc: "+cloneDoc.toString());

    }
复制代码

Output:设计模式

----------------  start  ----------------

txt: 今天是一个好天气
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

--------------------开始clone-----


 打印副本,看看数据  


----------------  start  ----------------

txt: 今天是一个好天气
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

  打印修改后的副本  


----------------  start  ----------------

txt: 副奔上修改文档:老龙王哭了
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

----看会不会影响原始文档-----


----------------  start  ----------------

txt: 今天是一个好天气
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------
  
内存地址:
wordDocument: com.devyk.android_dp_code.prototype.WordDocument@48533e64
cloneDoc: com.devyk.android_dp_code.prototype.WordDocument@64a294a6
复制代码

从上面代码跟打印能够看出 cloneDoc 是经过 wordDocument.clone() 建立的而且 cloneDoc 第一次输出和 wordDocument 原始文档数据同样,既 cloneDoc 是 wordDocument 的一份副本文件。难道这样就完了吗?不知道你们有没有注意这里的 mImagePath 字段,原始对象的 clone 方法这里至关把引用地址复制给了 clone 出来的对象,若是这 2 个对象中的任意一个对其修改,那么就会对原始数据形成破坏,失去了对数据的保护。那么怎么解决这个问题,请继续往下浏览(注意:经过 clone 的对象并不会执行 构造函数!)安全

浅拷贝和深拷贝

上述原型模式的实现实际上只是一个浅拷贝,也称为影子拷贝。这份拷贝实际上并非将原始文档的全部字段都从新构造了一份,而是副本文档的字段引用原始文档的字段。app

162bd6d2b1ba06809.png

咱们知道 A 引用 B 那么咱们能够认为 A,B 都指向同一个地址,当修改 A 时 B 也会随之改变, B 修改时 A 也会随之改变。咱们直接看下面代码示例:ide

@Test
    public void test4() {
        //1. 构建文档对象
        WordDocument wordDocument = new WordDocument();
        //2. 编辑文档
        wordDocument.setmTxt("今天是一个好天气");
        wordDocument.addImagepath("/sdcard/image.png");
        wordDocument.addImagepath("/sdcard/image2.png");
        wordDocument.addImagepath("/sdcard/image3.png");
        //打印文档内容
        wordDocument.println();


        System.out.println("--------------------开始clone-----\n\n");

        //以原始文档为准,copy 副本
        WordDocument cloneDoc = wordDocument.clone();

        System.out.println(" 打印副本,看看数据 \n\n");
        //打印副本,看看数据
        cloneDoc.println();

        //在副本文档上修改
        cloneDoc.setmTxt("副奔上修改文档:老龙王哭了");
        cloneDoc.addImagepath("/sdcard/副本发生改变");
        System.out.println(" 打印修改后的副本 \n\n");
        //打印修改后的副本
        cloneDoc.println();
        System.out.println("----看会不会影响原始文档-----\n\n");
        //看会不会影响原始文档???????
        wordDocument.println();

        System.out.println("内存地址:\nwordDocument: " + wordDocument.toString() + "\n" + "cloneDoc: " + cloneDoc.toString());

    }
复制代码

注意看副本文档,我手动调用 addImagepath 添加了一个新的图片地址。那么你们猜原始文档会发生改变吗?请看下面的输出:函数

----------------  start  ----------------

txt: 今天是一个好天气
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

--------------------开始clone-----


 打印副本,看看数据  


----------------  start  ----------------

txt: 今天是一个好天气
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

  打印修改后的副本  


----------------  start  ----------------

txt: 副奔上修改文档:老龙王哭了
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
path: /sdcard/副本发生改变
-----------------  end   ----------------

----看会不会影响原始文档-----


----------------  start  ----------------

txt: 今天是一个好天气
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
path: /sdcard/副本发生改变
-----------------  end   ----------------

内存地址:
wordDocument: com.devyk.android_dp_code.prototype.WordDocument@48533e64
cloneDoc: com.devyk.android_dp_code.prototype.WordDocument@64a294a6
复制代码

注意看咱们副本添加的图片地址是否是影响了原始文档的图片地址数据,那么这是怎么回事勒?对 C++ 了解的同窗应该深有体会,这是由于上文中 cloneDoc 只是进行了浅拷贝,图片列表 mImagePath 只是单纯的指向了 this.mImagePath , 并无从新构造一个 mImagePath 对象,就像开始介绍浅/深拷贝同样, A,B 对象其实指向的是同一个地址,因此无论 A,B 中任意一个对象改了指向地址的数据那么都会随之发生改变,那如何解决这个问题?答案就是采起深拷贝,即在拷贝对象时,对于引用型的字段也要采用拷贝的形式,而不是单纯引用形式,下面咱们修改 clone 代码,以下:

/** * 声明具有 clone 能力 * @return clone 的对象 */
    @Override
    public WordDocument clone() {
        try {
            WordDocument document = (WordDocument)super.clone();
            document.mTxt = this.mTxt;
            //进行深拷贝
            document.mImagePath = (ArrayList<String>) this.mImagePath.clone();
            return document;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
复制代码

再来测试一下,看输出类容:

----------------  start  ----------------

txt: 今天是一个好天气
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

--------------------开始clone-----


 打印副本,看看数据  


----------------  start  ----------------

txt: 今天是一个好天气
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

  打印修改后的副本  


----------------  start  ----------------

txt: 副奔上修改文档:老龙王哭了
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
path: /sdcard/副本发生改变
-----------------  end   ----------------

----看会不会影响原始文档-----


----------------  start  ----------------

txt: 今天是一个好天气
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

内存地址:
wordDocument: com.devyk.android_dp_code.prototype.WordDocument@48533e64
cloneDoc: com.devyk.android_dp_code.prototype.WordDocument@64a294a6
复制代码

经过输出内容,深拷贝解决了上述问题。

原型模式是一个很是简单的一个模式,它的核心问题就是对原始对象进行拷贝,在这个模式的使用过程当中须要注意一点就是 深/浅拷贝的问题。在实际开发中,为了减小没必要要的麻烦,建议你们都使用深拷贝。

这里若是对深浅拷贝感兴趣的话能够看掘金上这篇文章,不过是 JS 代码(了解原理就能够了),很火的一篇文章值得学习一下

源码中的原型模式

  • ArrayList

    刚刚咱们 clone 文档可知,进行的 ArrayList clone ,那么 ArrayList clone 具体是怎么实现的?咱们一块儿来看下:

    public Object clone() {
            try {
              //1. 
                ArrayList<?> v = (ArrayList<?>) super.clone();
              //2. 
                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(e);
            }
        }
    复制代码

    代码中第一步首先进行自身的 clone ,而后在对自身的数据进行 copy .

  • Intent

    下面以 Intent 来分析源码中的原型模式,首先看以下代码

    public static Intent toSMS(){
            Uri uri = Uri.parse("smsto:11202");
    
            Intent preIntent = new Intent(Intent.ACTION_SENDTO,uri);
            preIntent.putExtra("sms_body","test");
    
            //clone
            return (Intent) preIntent.clone();
    
        }
    
    复制代码

    2.png

    从代码中能够看到 preIntent.clone(); 方法拷贝了一个对象 Intent ,而后执行跳转 Activity,跳转的内容与原型数据一致。

    咱们继续看 Intent clone 具体实现:

    /***进行 clone **/  
    	@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;
            this.mLaunchToken = o.mLaunchToken;
            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 方法实际上在内部并无调用 super.clone() 来实现拷贝对象,而是经过 new Intent(this)。 在开始咱们提到过,使用 clone 和 new 须要根据构造对象的成原本决定,若是对象的构形成本比较高或者构造麻烦,那么使用 clone 函数效率较高,反之可使用 new 关键字的形式。这就是和 C++ 中的 copy 构造函数彻底一致,将原始对象做为构造函数的参数,而后在构造函数内将原始对象数据挨个 copy , 到此,整个 clone 过程就完成了。

总结

原型模式本质就是对象 copy ,与 C++ 中的拷贝构造函数类似,他们以前容易出现的问题也都是深拷贝、浅拷贝。使用原型模式能够解决构建复杂对象的资源消耗问题,可以在某些场景下提高建立爱你对象的效率。还有一个重要的用途,就是保护性拷贝,也就是某个对象对外可能只是只读模式。

优势:

原型模式是在内存中二进制流的 copy, 要比 new 一个对象性能好不少,特别是要在一个循环体内产生大量的对象时,原型模式能够更好地体现其优势。

缺点:

这既是它的有点也是缺点,直接在内存中拷贝,构造函数时不会执行的,在实际开发中应该注意这个潜在的问题。

文章代码地址

特别感谢

《 Android 源码设计模式解析与实战 》

相关文章
相关标签/搜索