「设计模式」🌓原型模式(Prototype)

1,404 阅读8分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

模式动机

假如你有一个对象,并希望生成一个完全相同的复制品,你该如何实现?

估计你是先新建一个属于相同类的对象,然后遍历其所有成员变量,并将成员变量的值复制到新对象中。

不错,想法很直接。但有一个问题,并非所有对象都可以通过这种方式直接复制,因为考虑到有些对象拥有私有成员变量 private,它们对外是不可见的。除此之外,你还需要知道对象所属类才能创建复制品,所以代码必须依赖该类。

基于以上的问题,原型模式就应运而生了,它的工作模式如下:

原型模式将克隆过程委派给被克隆的实际对象,模式为所有支持克隆的对象声明了一个通用接口(在 Java 中是实现 Cloneable 接口),该接口让你能够克隆对象,同时又无需代码和对象所属类耦合。支持克隆的对象即为原型,它实现的克隆方法使你无需新建一个对象。

细胞的有丝分裂就是原型模式一个很恰当的类比

定义

原型模式又称克隆模式,属于创建型模式,它能够复制已有对象,而不需要知道任何创建的细节,使代码不依赖对象所属的类。

UML 类图

模式结构

原型模式包含如下角色:

  • Prototype:抽象原型类对克隆方法进行声明,在绝大多数情况下,其中只会又一个名为 clone 的方法
  • ConcretePrototype:具体原型类将实现克隆方法(任何类都可以成为 ConcretePrototype,只需要实现 Prototype 接口即可)
  • Client:客户端可以复制实现了原型接口的任何对象

由于在 Java 中实现的方式比较特殊,与如上通用的 UML 类图实现的效果存在一些差异,我决定单独讲解下 Java 中原型模式的 UML 类图以及其实现。

Java 原型模式的 UML 类图

public class Object {
    // clone() is defined in Object class.
    protected native Object clone() throws CloneNotSupportedException;
}

所有的 Java 类都继承自 java.lang.Object,而 Object 类本身提供了一个 clone() 方法,可以将 Java 对象拷贝一份,因此所有类默认都实现了 clone() 方法,所以说 Java 语言中的原型模式实现很简单。

但是要注意一点:想要实现克隆的 Java 类必须实现一个标识接口 Cloneable,该接口是一个空接口,没有声明任何方法,仅作为标识接口,实现 implements 后表示这个 Java 类支持复制;如果一个类没有实现该接口但是调用了 clone() 方法,Java 编译器会抛出一个 CloneNotSupportedException 异常(如上代码块)。

⭐Tips:对于任何的对象 obj,clone() 方法满足:

  • obj.clone() != obj,即克隆对象与原对象不是同一个对象
  • obj.clone().getClass() == obj.getClass(),即克隆对象与原对象的类型一致
  • 如果对象 obj 的 equals() 方法定义恰当,那么 obj.clone().equals(x) 应当返回 true

更多实例

🌅关于浅克隆(浅拷贝)与深克隆(深拷贝)会在文末「模式扩展」部分详细讲解!

📧邮件复制(浅克隆)

由于邮件对象包含的内容较多(如发送者、接收者、标题、内容、日期、附件等),某系统中现需要提供一个邮件复制功能,对于已经创建好的邮件对象,可以通过复制的方式创建一个新的邮件对象,如果需要改变某部分内容,无须修改原始的邮件对象,只需要修改复制后得到的邮件对象即可。使用原型模式设计该系统。在本实例中使用浅克隆实现邮件复制,即复制邮件 Email 的同时不复制附件 Attachment 引用对象。

📧邮件复制(深克隆)

使用深克隆实现邮件复制,即复制邮件的同时也复制附件。

示例代码

Object.java

JDK 中的核心类

public class Object {
    // clone() is defined in Object class.
    protected native Object clone() throws CloneNotSupportedException;
}

Cloneable.java

JDK 中的核心类

package java.lang;

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

ConcretePrototype.java

public class ConcretePrototype implements Cloneable {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

Client.java

public class Client {
    // 原型对象
    private ConcretePrototype prototype = new ConcretePrototype();
    // 克隆对象
    private ConcretePrototype clone;

    public void operation() {
        try {
            clone = (ConcretePrototype) prototype.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        // false
        System.out.println(clone == prototype);
        // true
        System.out.println(clone.getClass() == prototype.getClass());
        // false: 因为未定义好 equals()
        System.out.println(clone.equals(prototype));
    }

    public static void main(String[] args) {
        new Client().operation();
    }
}

优缺点

✔你可以克隆对象,而无需与它们所属的具体类相耦合。

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

✔可以使用深克隆的方式保存对象的状态。

❌需要为每一个类配备一个克隆方法,如果对已有类进行改造,则必须修改其源代码,违背“开闭原则”。

❌实现深克隆时需要编写较为复杂的代码。

适用场景

在以下情况推荐使用原型模式:

(1)创建新对象成本较大。

(2)如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占内存不大的时候,也可以使用「原型模式」配合「备忘录模式」来应用。相反,如果对象的状态变化很大,或者对象占用的内存很大,那么采用「状态模式」会比「原型模式」更好。

「原型模式」落地

(1)很多软件提供的复制 Ctrl + C 和粘贴 Ctrl + V 操作就是原型模式的实际应用(CV 工程师狂喜)。

(2)在 Spring 中,用户也可以采用原型模式来创建新的 bean 实例,从而实现每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响。

模式扩展

深浅克隆

通常情况下,一个类包含一些成员对象,在使用原型模式克隆对象时,根据其成员对象「引用对象」是否也克隆,原型模式又可以分为两种形式:深克隆和浅克隆。

🔎Java 中有三种 “复制” 对象的方式:对象赋值(=)、浅克隆(implements Cloneable)、深克隆(implements Serializable)。

📈三图看懂直接赋值 =、浅克隆 Cloneable、深克隆 Serializable

直接赋值

浅克隆

深克隆

浅克隆

浅克隆需要实现 Cloneable 接口,并借助 clone() 方法,上文实现的都是浅克隆。

深克隆

深克隆需要实现 Serializable 接口,并借助 ByteArrayOutputStreamByteArrayInputStream 类:将对象读入流 (Stream) 中 (序列化),然后将其读出 (反序列化),即可完成对象的深克隆。

注意,写入到流中的对象是原对象的一个拷贝,而原对象仍存在于 JVM 中!

Email.java

public class Email implements Serializable {

    private Attachment attachment;

    public Email() {
        // TODO
    }

    public Object deepClone() throws IOException, ClassNotFoundException {
        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(this);
        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        return ois.readObject();
    }

    public void display() {
        // TODO
    }

    public Attachment getAttachment() {
        return attachment;
    }
}

Attachment.java

成员对象也需要实现序列化接口

public class Attachment implements Serializable {
    public void download() {
        // TODO
    }
}

带原型注册表的原型模式

在原型模式的基础上创建一个中心化原型注册表,用于存储常用原型。

你可以新建一个工厂类来实现注册表,或者在原型基类中添加一个获取原型的静态方法。

PrototypeManager 关键代码:

// 带原型管理器的原型模式: 如果 Prototype 有多个子类 ConcretePrototypeA、ConcretePrototypeB... 使用带有原型管理器的原型模式十分方便
public class PrototypeManager {
    // prototype-pattern with prototypeManager
    static Map<String, Prototype> prototypeManager;

    // all types
    static {
        prototypeManager = new Hashtable<>();
        // ConcretePrototypeA1 & ConcretePrototypeA2 are ConcretePrototypeA's SubClass
        prototypeManager.put("A", new ConcretePrototypeA());
        prototypeManager.put("B", new ConcretePrototypeB());
    }

    public static void add(String key, Prototype prototype) {
        prototypeManager.put(key, prototype);
    }

    public static Prototype get(String key) {
        try {
            return (Prototype) prototypeManager.get(key).clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

最后

👆上一篇:「设计模式」🚀建造者模式(Builder)

👇下一篇:「设计模式」🌍单例模式(Singleton)

❤️ 好的代码无需解释,关注「手撕设计模式」专栏,跟我一起学习设计模式,你的代码也能像诗一样优雅!

❤️ / END / 如果本文对你有帮助,点个「赞」支持下吧,你的支持就是我最大的动力!