设计模式 | 原型模式及典型应用

前言

本文的主要内容如下:

  • 介绍原型模式
  • 示例
    • Java语言的clone
    • 浅克隆与深克隆
    • 实现深克隆
  • 原型模式的典型应用

原型模式

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

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

原型模式是一种“另类”的创建型模式,创建克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。

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

角色

  • Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
  • ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
  • Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。

原型模式的核心在于如何实现克隆方法

示例

Java语言提供的clone()方法

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

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

public class Mail implements Cloneable{
    private String name;
    private String emailAddress;
    private String content;
    public Mail(){
        System.out.println("Mail Class Constructor");
    }
    // ...省略 getter、setter
    @Override
    protected Object clone() throws CloneNotSupportedException {
        System.out.println("clone mail object");
        return super.clone();
    }
}

在客户端创建原型对象和克隆对象也很简单,如下代码所示:

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Mail mail = new Mail();
        mail.setContent("初始化模板");
        System.out.println("初始化mail:"+mail);
        for(int i = 0;i < 3;i++){
            System.out.println();
            Mail mailTemp = (Mail) mail.clone();
            mailTemp.setName("姓名"+i);
            mailTemp.setEmailAddress("姓名"+i+"@test.com");
            mailTemp.setContent("恭喜您,此次抽奖活动中奖了");
            MailUtil.sendMail(mailTemp);
            System.out.println("克隆的mailTemp:"+mailTemp);
        }
        MailUtil.saveOriginMailRecord(mail);
    }
}

其中的 MailUtil 工具类为

public class MailUtil {
    public static void sendMail(Mail mail) {
        String outputContent = "向{0}同学,邮件地址:{1},邮件内容:{2}发送邮件成功";
        System.out.println(MessageFormat.format(outputContent, mail.getName(), mail.getEmailAddress(), mail.getContent()));
    }

    public static void saveOriginMailRecord(Mail mail) {
        System.out.println("存储originMail记录,originMail:" + mail.getContent());
    }
}

输出如下:

Mail Class Constructor
初始化mail:Mail{name='null', emailAddress='null', content='初始化模板'}com.designpattern.prototype.Mail@12edcd21

clone mail object
向姓名0同学,邮件地址:姓名[email protected],邮件内容:恭喜您,此次抽奖活动中奖了发送邮件成功
克隆的mailTemp:Mail{name='姓名0', emailAddress='姓名[email protected]', content='恭喜您,此次抽奖活动中奖了'}com.designpattern.prototype.Mail@34c45dca

clone mail object
向姓名1同学,邮件地址:姓名[email protected],邮件内容:恭喜您,此次抽奖活动中奖了发送邮件成功
克隆的mailTemp:Mail{name='姓名1', emailAddress='姓名[email protected]', content='恭喜您,此次抽奖活动中奖了'}com.designpattern.prototype.Mail@52cc8049

clone mail object
向姓名2同学,邮件地址:姓名[email protected],邮件内容:恭喜您,此次抽奖活动中奖了发送邮件成功
克隆的mailTemp:Mail{name='姓名2', emailAddress='姓名[email protected]', content='恭喜您,此次抽奖活动中奖了'}com.designpattern.prototype.Mail@5b6f7412
存储originMail记录,originMail:初始化模板

从输出结果中我们可以观察到:

  • for循环中的 mailTemp 从 mail 对象中克隆得到,它们的内存地址均不同,说明不是同一个对象,克隆成功,克隆仅仅通过调用 super.clone() 即可。
  • 最后调用的 MailUtil.saveOriginMailRecord(mail); 中的 mail 对象的内容仍为 for 循环之前设置的内容,并没有因为克隆而改变。
  • 克隆的时候调用了 clone 方法,并没有调用 Mail 类的构造器,只在最前面 new 的时候才调用了一次

关于输出的内存地址是怎么输出的,我们还需要看一下 Object#toString 方法

public class Object {
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    //...省略...
}

所以所谓的内存地址即为 hashCode() 的十六进制表示,这里简单的认为 内存地址相同则为同一个对象,不同则为不同对象

再来看一眼 Object#clone 方法

protected native Object clone() throws CloneNotSupportedException;

这是一个 native 关键字修饰的方法

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

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

为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:

  1. 在派生类中覆盖基类的 clone() 方法,并声明为public;
  2. 在派生类的 clone() 方法中,调用 super.clone()
  3. 派生类需实现Cloneable接口。

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

浅克隆与深克隆

看下面的示例

public class Pig implements Cloneable{
    private String name;
    private Date birthday;
    // ...getter, setter, construct
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Pig pig = (Pig)super.clone();
        return pig;
    }
    @Override
    public String toString() {
        return "Pig{" +
                "name='" + name + '\'' +
                ", birthday=" + birthday +
                '}'+super.toString();
    }
}

测试

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Date birthday = new Date(0L);
        Pig pig1 = new Pig("佩奇",birthday);
        Pig pig2 = (Pig) pig1.clone();
        System.out.println(pig1);
        System.out.println(pig2);

        pig1.getBirthday().setTime(666666666666L);

        System.out.println(pig1);
        System.out.println(pig2);
    }
}

输出如下

Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@27973e9b
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@312b1dae
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@27973e9b
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@312b1dae

我们照着上一小节说的实现 Cloneable,调用 super.clone(); 进行克隆,中间我们对 pig1 对象设置了一个时间戳,从输出中我们可以发现什么问题呢?

我们可以发现:

  • pig1pig2 的内存地址不同
  • pig1 设置了时间,同事 pig2 的时间也改变了

我们通过 debug 来看一下

设计模式 | 原型模式及典型应用_第1张图片
debug查看对象地址

发现如下:

  • pig1 与 pig2 地址不一样
  • pig1 的 birthday 与 pig2 的 birthday 一样

这里引出浅拷贝与深拷贝。

在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。

浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。

浅克隆:

  • 在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

  • 简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制

  • 在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。

深克隆:

  • 在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。

  • 简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

  • 在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

实现深克隆

方式一,手动对引用对象进行克隆:

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Pig pig = (Pig)super.clone();

        //深克隆
        pig.birthday = (Date) pig.birthday.clone();
        return pig;
    }

方式二,通过序列化的方式:

public class Pig implements Serializable {
    private String name;
    private Date birthday;
    // ...省略 getter, setter等

    protected Object deepClone() throws CloneNotSupportedException, IOException, ClassNotFoundException {
        //将对象写入流中
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(this);

        //将对象从流中取出
        ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (ois.readObject());
    }
}
设计模式 | 原型模式及典型应用_第2张图片
序列化方式的深克隆结果

破坏单例模式

饿汉式单例模式如下:

public class HungrySingleton implements Serializable, Cloneable {

    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {
        if (hungrySingleton != null) {
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
    private Object readResolve() {
        return hungrySingleton;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

使用反射获取对象,测试如下

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        Method method = hungrySingleton.getClass().getDeclaredMethod("clone");
        method.setAccessible(true);
        HungrySingleton cloneHungrySingleton = (HungrySingleton) method.invoke(hungrySingleton);
        System.out.println(hungrySingleton);
        System.out.println(cloneHungrySingleton);
    }
}

输出

com.designpattern.HungrySingleton@34c45dca
com.designpattern.HungrySingleton@52cc8049

可以看到,通过原型模式,我们把单例模式给破坏了,现在有两个对象了

为了防止单例模式被破坏,我们可以:不实现 Cloneable 接口;或者把 clone 方法改为如下

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }

原型模式的典型应用

  1. Object 类中的 clone 接口
  2. Cloneable 接口的实现类,可以看到至少一千多个,找几个例子譬如:
设计模式 | 原型模式及典型应用_第3张图片
Cloneable接口的实现类

ArrayListclone 的重写如下:

public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable {
    public Object clone() {
        try {
            ArrayList v = (ArrayList) super.clone();
            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);
        }
    }
    //...省略
}

调用 super.clone(); 之后把 elementData 数据 copy 了一份

同理,我们看看 HashMapclone 方法的重写:

public class HashMap extends AbstractMap implements Map, Cloneable, Serializable {
    @Override
    public Object clone() {
        HashMap result;
        try {
            result = (HashMap)super.clone();
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
        result.reinitialize();
        result.putMapEntries(this, false);
        return result;
    }
    // ...省略...
}

mybatis 中的 org.apache.ibatis.cache.CacheKeyclone 方法的重写:

public class CacheKey implements Cloneable, Serializable {
    private List updateList;
    public CacheKey clone() throws CloneNotSupportedException {
        CacheKey clonedCacheKey = (CacheKey)super.clone();
        clonedCacheKey.updateList = new ArrayList(this.updateList);
        return clonedCacheKey;
    }
    // ... 省略...
}
 
 

这里又要注意,updateListList 类型,所以可能是值类型的List,也可能是引用类型的List,克隆的结果需要注意是否为深克隆或者浅克隆

使用原始模式的时候一定要注意为深克隆还是浅克隆。

原型模式总结

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

  • 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
  • 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
  • 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
  • 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。

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

  • 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
  • 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。

适用场景:

  • 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
  • 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
  • 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。

参考:
刘伟:设计模式Java版
慕课网java设计模式精讲 Debug 方式+内存分析

后记

欢迎评论、转发、分享,您的支持是我最大的动力

更多内容可访问我的个人博客:http://laijianfeng.org

关注【小旋锋】微信公众号,及时接收博文推送

设计模式 | 原型模式及典型应用_第4张图片
关注_小旋锋_微信公众号

你可能感兴趣的:(设计模式 | 原型模式及典型应用)