原型模式

简介

Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

原型模式(Prototype Pattern) 是 创建型 设计模式之一,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象相似的新对象。

建造者模式 用于创建 复杂对象,而 原型模式 用于创建 初始化耗时较长的对象

原型模式 的核心在于 拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能提升许多。

主要解决

当对象的构建过程比较耗时时,可以利用当前系统中已存在的对象作为原型,对其进行克隆(基于二进制流的复制),躲避初始化过程,使得新对象的创建时间大大减少。

原型模式优缺点

优点:

  • 性能优良,Java自带的 原型模式 是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
  • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。

缺点

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

使用场景

  • 资源优化:创建对象成本较大(例如初始化时间长,占用CPU多或占太多网络资源)等。
  • 性能与安全优化:创建一个对象需要繁琐的数据准备或访问权限等。
  • 系统中存在大量多处使用该类对象,且各个调用者都需要修改其值时。

模式讲解

首先看下 原型模式 UML 类图:

原型模式_第1张图片
原型模式

从 UML 图中,我们可以看到,原型模式 主要包含三个角色:

  • 客户(Client):客户类提出创建对象的请求。
  • 抽象原型(Prototype):规定拷贝接口。
  • 具体原型(Concrete Prototype):被拷贝的对象。

总结:对不通过new关键字,而是通过对象拷贝来实现创建对象的模式就称作 原型模式。

代码展示

如果按照上文 原型模式 UML 类图,实现的源码如下所示:

class Client {
    //客户类
    public static void main(String[] args) {
        //创建原型对象
        ConcretePrototype type = new ConcretePrototype("original");
        System.out.println(type);
        //拷贝原型对象
        ConcretePrototype cloneType = type.clone();
        cloneType.desc = "clone";
        System.out.println(cloneType); 

    }

    //抽象原型
    interface IPrototype {
        T clone();
    }

    //具体原型
    static class ConcretePrototype implements IPrototype {
        private String desc;

        public ConcretePrototype(String desc) {
            this.desc = desc;
        }

        @Override
        public ConcretePrototype clone() {
            //进行拷贝
            return new ConcretePrototype(this.desc);
        }

        @Override
        public String toString(){
            return String.format("[%s--desc: %s]",this.getClass().getSimpleName(),this.desc);
        }
    }
}

但是在 Java 中,我们无需自己手动去创建 抽象原型 接口,因为 Java 已经内置了 Cloneable 这个 抽象原型 接口,我们自定义的类型,只需实现该接口,并覆写Object.clone() 方法即可完成本类的复制。

我们查看 Cloneable 的 API 文档,可以看到,它是一个空接口。Java 提供的 Cloneable 接口只起一个作用,就是在运行时期通知 Java 虚拟机可以安全地在这个类上使用 clone() 方法。而如果类没有实现 Cloneable 接口,则调用 clone() 方法会抛出 CloneNotSupportedException 异常

一般而言,clone() 方法的实现需满足以下的描述:

  1. 对任何的对象x,都有:x.clone() != x。换言之,拷贝对象与原对象不是同一个对象。
  2. 对任何的对象x,都有:x.clone().getClass() == x.getClass(),换言之,拷贝对象与原对象的类型一样。
  3. 如果对象x的equals()方法定义其恰当的话,那么 x.clone().equals(x) 应当成立的。

工程师在设计自定义类的 clone() 方法时,也应当遵守着三个条件。一般来说,上面的三个条件中的前两个是必需的,而第三个是可选的。

下面我们直接使用 Java 提供的 API 改写上文那个 原型模式 实现,代码如下:

class Client {
    public static void main(String[] args) {
        // 创建原型对象
        ConcretePrototype type = new ConcretePrototype("original");
        System.out.println(type);
        // 拷贝原型对象
        ConcretePrototype cloneType = type.clone();
        cloneType.desc = "clone";
        System.out.println(cloneType);

    }

    static class ConcretePrototype implements Cloneable {
        private String desc;

        public ConcretePrototype(String desc) {
            this.desc = desc;
        }

        @Override
        protected ConcretePrototype clone() {
            ConcretePrototype cloneType = null;
            try {
                cloneType = (ConcretePrototype) super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return cloneType;
        }

        @Override
        public String toString() {
            return String.format("[%s--desc: %s]", this.getClass().getSimpleName(), this.desc);
        }
    }
}

注意

  • super.clone()直接是从内存(具体地说是堆内存)中以二进制流的方式进行拷贝,重新分配一个内存块,因此其效率很高(同时,由于是基于内存拷贝,对象的构造函数是不会被调用的,也就是无需经历初始化过程)。
  • super.clone()进行的是 浅拷贝。如果类中存在引用对象属性,则原型与拷贝对象内该属性指向同一对象。

使用 Java 自带的拷贝机制,clone() 方法进行的是浅拷贝。而如果我们想进行深拷贝,可以直接在super.clone()后,手动给拷贝对象相关属性分配另一块内存,不过如果原型对象维护很多引用属性的时候,手动分配会比较繁琐。因此,在 Java 中,如果想完成原型对象的深度拷贝,通常使用 序列化(Serializable)的方式,代码如下所示:

public class Client {
    public static void main(String[] args) {
        // 创建原型对象
        ConcretePrototype type = new ConcretePrototype();
        System.out.println(type);
        // 拷贝原型对象
        ConcretePrototype cloneType = type.deepClone();
        System.out.println(cloneType);

    }

    interface IPrototype {
        T deepClone();
    }

    public static class ConcretePrototype implements IPrototype, Serializable {
        private static final long serialVersionUID = 1L;
        private final List list = new ArrayList();

        @Override
        public String toString() {
            return String.format("list address: %x", System.identityHashCode(this.list));
        }

        @Override
        public ConcretePrototype deepClone() {
            ConcretePrototype deepCloneType = null;
            ObjectOutputStream oos = null;
            ObjectInputStream ois = null;
            try {
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                oos = new ObjectOutputStream(bos);
                // 将对象写到流里
                oos.writeObject(this);
                // 从流里读回来
                ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
                deepCloneType = (ConcretePrototype) ois.readObject();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (ois != null) {
                        ois.close();
                    }
                    if(oos != null){
                        oos.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return deepCloneType;
        }
    }
}

这里为了不与 Java 自带的 clone() 方法重叠,我们自己按照本文刚开始的自定义 原型模式 方法,自己定义了一个支持深度拷贝的 抽象原型具体原型

具体原型ConcretePrototype 采用 序列化(Serializable)的方式以流的形式拷贝自己(这里特地将引用变量list声明成final类型,为的就是不让自己手动更改指向,强制必须使用 序列化(Serializable)),并且将 具体原型ConcretePrototypetoString方法返回内部引用属性的地址,这样,在客户端进行拷贝后,我们通过输出两者的地址空间,就知道是否是同一个对象指向。

代码运行结果如下:


result

可以看到,我们的确完成了深拷贝。

你可能感兴趣的:(原型模式)