创建型设计模式之原型模式

原型模式的介绍

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

原型模式定义

用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象

原型模式使用场景

(1)类初始化需要消耗非常多的资源,通过原型拷贝避免这些消耗

(2)new 一个对象需要非常繁琐的数据准备或访问权限。

(3)一个对象需要提供给其他对象访问,而且各个调用者可能需要修改其值,可以考虑使用原型模式拷贝多个对象供调用者使用,即保护性拷贝

注意:通过实现Cloneable接口的原型模式在调用clone函数构造实例时并不一定比new操作快,只有当new对象较为耗时或成本较高时,通过clone方法才能有效率上的提高。因此在使用原型模式是要考虑构建对象的成本和做一些效率上的测试。 

原型模式的类图

创建型设计模式之原型模式

角色介绍:

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

原型模式的简单实现

假设现在有一份文档,文档中有文字和图片,某位用户想要在文档上做一些内容修改,但是修改后的文档是否被采用是不确定的,所以用户需要将当前文档拷贝一份,然后在文档副本上进行修改。这个原始文档就是我们上述所说的样本实例,也就是将要被克隆的对象,我们称之为原型。

文档类型,扮演的是ConcretePrototype角色,Cloneable代表的是Prototype角色

public class WorkDocument implements Cloneable {

    private String mText;//文本
    private ArrayList<String> mImages = new ArrayList<>();//图片列表

    public WorkDocument(){
        System.out.println("WorkDocument的构造函数");
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        WorkDocument document = (WorkDocument) super.clone();
        this.mText = document.mText;
        this.mImages = document.mImages;
        return document;
    }

    public void showDocument(){
        System.out.println("============start===========");
        System.out.println("Text: " + mText);
        System.out.println("Image List:");
        for(String s : mImages){
            System.out.println("Image name: " + s);
        }
        System.out.println("=============end============");
    }

    public String getmText() {
        return mText;
    }

    public void setmText(String mText) {
        this.mText = mText;
    }

    public ArrayList<String> getmImages() {
        return mImages;
    }

    public void setmImage(String mImages) {
        this.mImages.add(mImages);
    }
}

下面看Client端

public class Client {

    public static void main(String[] args) throws CloneNotSupportedException {

        //1、创建文档对象
        WorkDocument originDoc = new WorkDocument();
        //2、编辑文档
        originDoc.setmText("这是一篇文档");
        originDoc.setmImage("图片1");
        originDoc.setmImage("图片2");
        originDoc.setmImage("图片3");
        originDoc.showDocument();
        //以原型文档为原型,拷贝一份副本
        WorkDocument doc2 = (WorkDocument) originDoc.clone();
        System.out.println("拷贝后:");
        doc2.showDocument();
        doc2.setmText("这是修改后的文本");
        System.out.println("修改后的副文本:");
        doc2.showDocument();
        System.out.println("修改后的原文本:");
        originDoc.showDocument();
    }

}

输出结果

WorkDocument的构造函数
============start===========
Text: 这是一篇文档
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
=============end============
拷贝后:
============start===========
Text: 这是一篇文档
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
=============end============
修改后的副文本:
============start===========
Text: 这是修改后的文本
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
=============end============
修改后的原文本:
============start===========
Text: 这是一篇文档
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
=============end============

从上面可以看出,doc2是originDoc的一份拷贝,它们的内容一样,doc2修改文本后并不会影响originDoc的文本。而且通过clone拷贝对象时并不会执行构造函数,如果需要在构造函数中做某些特殊的初始化操作时,在使用clone时要注意构造函数不会执行。


浅拷贝和深拷贝

上面的原型模式只是一个浅拷贝,这份拷贝并不是将原始的文档的所有字段都重新构造了一份,而是副文本的字段引用原始文本的字段

也就是说当我们修改副文本时,原文本也会被修改,修改一下main函数:

public class Client {

    public static void main(String[] args) throws CloneNotSupportedException {

        //1、创建文档对象
        WorkDocument originDoc = new WorkDocument();
        //2、编辑文档
        originDoc.setmText("这是一篇文档");
        originDoc.setmImage("图片1");
        originDoc.setmImage("图片2");
        originDoc.setmImage("图片3");
        originDoc.showDocument();
        //以原型文档为原型,拷贝一份副本
        WorkDocument doc2 = (WorkDocument) originDoc.clone();
        System.out.println("拷贝后:");
        doc2.showDocument();
        doc2.setmText("这是修改后的文本");
        doc2.setmImage("你好.png");//
        System.out.println("修改后的副文本:");
        doc2.showDocument();
        System.out.println("修改后的原文本:");
        originDoc.showDocument();
    }

}

输出

WorkDocument的构造函数
============start===========
Text: 这是一篇文档
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
=============end============
拷贝后:
============start===========
Text: 这是一篇文档
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
=============end============
修改后的副文本:
============start===========
Text: 这是修改后的文本
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
Image name: 你好.png
=============end============
修改后的原文本:
============start===========
Text: 这是一篇文档
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
Image name: 你好.png
=============end============

我在doc2添加了一张“你好.png” 图片,输出结果是原文档和副文档都改变了,为什么呢?因为上文中doc2中的this.mImages只是单纯的指向originDoc中的this.mImages,并没有重新构造一个mImages对象,所以修改doc2等于修改originDoc,那么如何解决呢?答案就是采用深拷贝,即在拷贝对象时,对引用型字段也要进行拷贝形式,clone修改如下:

@Override
    protected Object clone() throws CloneNotSupportedException {
        WorkDocument document = (WorkDocument) super.clone();
        this.mText = document.mText;
        //对mImages对象也要进行调用clone函数,深拷贝
        this.mImages = (ArrayList<String>) document.mImages.clone();
        return document;
    }

输出

WorkDocument的构造函数
============start===========
Text: 这是一篇文档
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
=============end============
拷贝后:
============start===========
Text: 这是一篇文档
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
=============end============
修改后的副文本:
============start===========
Text: 这是修改后的文本
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
Image name: 你好.png
=============end============
修改后的原文本:
============start===========
Text: 这是一篇文档
Image List:
Image name: 图片1
Image name: 图片2
Image name: 图片3
=============end============

总结

原型模式是一个很简单的模式,就是对原始对象进行拷贝,在开发过程中为了减少错误,建议大家使用深拷贝,避免操作副本时影响原始对象。使用原型模式可以解决构建复杂对象的资源消耗问题,还有一个重要用途是保护原对象,原对象对外只是可读的。

优点:

(1)原型模式是在内存中二进制流的拷贝,要比直接new一个对象性能好,特别要在一个循环体内产生大量对象时,使用原型模式优点更明显

缺点:

(2)构造函数不会执行