【笔记】设计模式 -- 原型模式

参考:
[1] 史上最全设计模式导学目录(完整版)
[2] 《Android源码设计模式解析与实战》

1. 简介

原型模式是一个创建型模式。
原型模式,顾名思义需要一个原型作为模板,使得我们可以通过这个原型对象复制出内部属性一致的新对象,即通过一个原型对象克隆出多个一模一样的对象。原型模式多用于创建复杂的或构造耗时的实例,因为复制一个已经存在的实例可是程序运行更高效。

由于我们经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在实际开发中的使用频率还是非常高的。

2. 定义

原型模式(Prototype Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是对原型对象的克隆。

3. 使用场景

在以下情况下可以考虑使用原型模式:

·1> 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
·2> 通过new产生一个对象需要非常繁琐的数据准备或访问权限,这时可以使用原型模式。
·3> 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用,即保护性拷贝。

4. 原型模式的实现

原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。原型模式的核心在于如何实现克隆方法。

需要注意: 通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过对这些克隆对象的修改,可得到一系列相似但不完全相同的对象。

原型模式一般包含以下角色:
(1)Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型的公共父类,可以是抽象类,也可以是接口,甚至还可以是具体实现类。
(2)ConcretePrototype(具体原型类):它实现抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象(新对象)。
(3)Client(客户类):让一个原型对象克隆自身从而创建一个新的对象。客户类可直接实例化具体原型类,通过调用它的克隆方法获取一个新的对象。根据需求选择具体的客户类。

4.1 原型模式的通用实现

通用的克隆实现方法是在具体原型类的克隆方法中,实例化一个与自身类型相同的对象(本类对象),并将其返回,同时将相关参数传入新创建的对象中,保证它们的成员属性相同。示例代码如下所示:

抽象原型类

public interface Prototype {
    
    

    Prototype copy();
}

具体原型类

public class APrototype implements Prototype {
    
    

    private String attr;

    public String getAttr() {
    
    
        return attr;
    }

    public void setAttr(String attr) {
    
    
        this.attr = attr;
    }

    @Override
    public String toString() {
    
    
        return "APrototype{" +
                "attr='" + attr + '\'' +
                '}';
    }

    @Override
    public Prototype copy() {
    
    
        APrototype aPrototype = new APrototype();//创建新对象
        aPrototype.setAttr(attr);
        return aPrototype;
    }
}

客户端调用

//原型类实例
APrototype aPrototype = new APrototype();
aPrototype.setAttr("prototype");
System.out.println("aPrototype: " + aPrototype.toString());

//原型类实例aPrototype,通过调用copy()方法,克隆(复制)一个新对象
//新对象copy可设置属性
APrototype copy = (APrototype) aPrototype.copy();

System.out.println("copy: " + copy.toString());
copy.setAttr("clone");
System.out.println("copy: " + copy.toString());

System.out.println("aPrototype: " + aPrototype.toString());

打印

aPrototype: APrototype{attr='prototype'}
copy: APrototype{attr='prototype'}
copy: APrototype{attr='clone'}
aPrototype: APrototype{attr='prototype'}

通过打印可以看到,通过调用copy()方法,克隆了一个新对象copy,它的属性值与原型对象相同,copy的属性值可以通过setter方法进行更改。

这种方法可作为原型模式的通用实现,它与编程语言特性无关,任何面向对象语言都可以使用这种形式来实现对原型的克隆。

4.2 Java提供的clone()方法

学过Java语言的人都知道,所有的Java类都继承自java.lang.Object。事实上,Object类提供一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。

需要注意的是能够实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedException异常。

一般而言,Java语言中的clone()方法满足:

(1) 对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;
(2) 对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一样;
(3) 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。

为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:
(1) 在派生类中覆盖基类的clone()方法,并声明为public;
(2) 在派生类的clone()方法中,调用super.clone();
(3)派生类需实现Cloneable接口。

此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类。

在我们工作汇报的时候,有一个日报、周报、月报等工作汇报。以日报为例,当我们以文档形式编辑好了一个日报,这个日报还有待修改,当有可能反复修改时,为了能够保护已有内容,或需要进行对比和参照,我们通常会拷贝出一个副本,对新的日报副本进行编辑,类似于保护性拷贝。那么,原始的日报就是一个模板实例,也就是将要被克隆的对象,我们称之为原型

日报类(原型类)
定义了属性:name(姓名)、content(日报内容)、submitTime(提交时间)和 imageList(图片列表)。
实现了Java的 Cloneable 接口,重写了 clone() 方法。
提供了打印报告方法:printReport()

public class DailyReport implements Cloneable {
    
    

    private String name;

    private String content;

    private String submitTime;

    private ArrayList<String> imageList = new ArrayList<>();


    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public String getContent() {
    
    
        return content;
    }

    public void setContent(String content) {
    
    
        this.content = content;
    }

    public String getSubmitTime() {
    
    
        return submitTime;
    }

    public void setSubmitTime(String submitTime) {
    
    
        this.submitTime = submitTime;
    }

    public ArrayList<String> getImageList() {
    
    
        return imageList;
    }

    public void addImage(String img) {
    
    
        this.imageList.add(img);
    }


    public void printReport(){
    
    
        System.out.println("========DailyReport========");
        System.out.println("name:" + name);
        System.out.println("content:" + content);
        System.out.println("submitTime:" + submitTime);
        System.out.println("imageList:" + imageList);
        System.out.println("==========================");
    }

    @Override
    protected DailyReport clone() {
    
    

        DailyReport dailyReport = null;
        try {
    
    
            dailyReport = (DailyReport) super.clone();

            dailyReport.name = this.name;
            dailyReport.content = this.content;
            dailyReport.submitTime = this.submitTime;
            dailyReport.imageList = (ArrayList<String>) this.imageList;
        } catch (CloneNotSupportedException e) {
    
    
            e.printStackTrace();
        }
        return dailyReport;
    }

}

clone()方法返回值类型为当前类,方法中将对象的属性重新赋给了克隆对象(新对象)。

注意: clone()方法不是接口 Cloneable 的方法,而是Object中的方法,Cloneable只是一个标识接口,表明这个类是可拷贝的。

下面看客户端调用:

DailyReport dailyReport = new DailyReport();
dailyReport.setName("Andy");
dailyReport.setContent("Complete part 1.");
dailyReport.setSubmitTime("2019.1.4");
dailyReport.addImage("img1-1.png");
dailyReport.addImage("img1-2.png");
dailyReport.printReport();//打印原型

DailyReport clone = dailyReport.clone();
clone.printReport();//克隆后打印
clone.setName("Jerry");
clone.setContent("Complete part 2.");
clone.printReport();//修改属性后打印

打印结果如下

========DailyReport========
name:Andy
content:Complete part 1.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png]
==========================
========DailyReport========
name:Andy
content:Complete part 1.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png]
==========================
========DailyReport========
name:Jerry
content:Complete part 2.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png]
==========================
========DailyReport========
name:Andy
content:Complete part 1.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png]
==========================

通过打印结果我们发现,通过拷贝获得的对象属性与原型形同,修改克隆对象的文本属性后,原型的文本属性不受影响,这就保证了原型的安全性。

还需要注意的是,**通过clone方法拷贝对象时不会执行构造方法。**因此,如果在构造方法中需要一些特殊的初始化的类型,在使用Cloneable实现拷贝时,需要注意构造函数不会执行的问题。

4.3 浅拷贝和深拷贝

1.浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
2.深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。


在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

上述原型实现其实是一个浅拷贝,也成为影子拷贝,这份拷贝实际上并不是将原型中所有字段都重新构造了一份,而是副本的字段引用了原型的字段。

我们知道A引用B表示两个对象指向同一个地址。当A修改时B也会改变,B修改时A同样也会改变。

我们将克隆之后的代码改变一下:

DailyReport clone = dailyReport.clone();
clone.printReport();//克隆后打印
clone.setName("Jerry");
clone.setContent("Complete part 2.");
clone.addImage("img2-1.png");
clone.printReport();//修改属性后打印
dailyReport.printReport();//再次打印原型

打印结果如下

========DailyReport========
name:Andy
content:Complete part 1.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png]
==========================
========DailyReport========
name:Andy
content:Complete part 1.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png]
==========================
========DailyReport========
name:Jerry
content:Complete part 2.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png, img2-1.png]
==========================
========DailyReport========
name:Andy
content:Complete part 1.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png, img2-1.png]
==========================

我们会发现,当imageList中添加一个item后,原型的imgeList也发生了改变,也增加了一个相同的item。这就是因为原型中的clone方法中只简单进行了浅拷贝,副本对象的imageList只是简单指向(=)了原型中的imageList的引用,并没有重新构建一个imageList对象。这导致原型和副本的imageList表示的是同一个对象,其中一个添加了item,另一个也会多一个,因为本身就是同一个对象。为了解决这个问题,在拷贝对象时,对于引用型的字段也要采用拷贝的形式,而不是单纯的引用形式。clone方法修改如下:

@Override
protected DailyReport clone() {
    
    
    DailyReport dailyReport = null;
    try {
    
    
        dailyReport = (DailyReport) super.clone();
        dailyReport.name = this.name;
        dailyReport.content = this.content;
        dailyReport.submitTime = this.submitTime;
        dailyReport.imageList = (ArrayList<String>) this.imageList.clone();
    } catch (CloneNotSupportedException e) {
    
    
        e.printStackTrace();
    }
    return dailyReport;
}

打印结果

========DailyReport========
name:Andy
content:Complete part 1.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png]
==========================
========DailyReport========
name:Andy
content:Complete part 1.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png]
==========================
========DailyReport========
name:Jerry
content:Complete part 2.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png, img2-1.png]
==========================
========DailyReport========
name:Andy
content:Complete part 1.
submitTime:2019.1.4
imageList:[img1-1.png, img1-2.png]
==========================

上面的日志可以通过序列化实现深拷贝,只需要实现Serializable接口,通过I/O流实现深拷贝,代码如下

public DailyReport deepClone() {
    
    

    DailyReport dailyReport = null;
    try {
    
    
        //将对象写入流中
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(this);

        //将对象从流中取出
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        dailyReport = ((DailyReport) ois.readObject());
        
    } catch (IOException | ClassNotFoundException e) {
    
    
        e.printStackTrace();
    }
    return dailyReport;
}

5. 总结

原型模式的核心问题是对原始对象的拷贝,需要特别注意的是深、浅拷贝的问题。开发中应尽量使用深拷贝,避免操作副本影响原始对象的问题。

1.主要优点

原型模式的主要优点如下:

(1) 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。

(2) 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。

(3) 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。

(4) 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。

2.主要缺点

原型模式的主要缺点如下:

(1) 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。

(2) 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。

猜你喜欢

转载自blog.csdn.net/wangxiaocheng16/article/details/85764038
今日推荐