设计模式-原型模式

1,757 阅读4分钟

原型模式是创建型模式的一种,其特点在于通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”,这个原型是可定制的。

原型模式的核心在于拷贝源对象,UML类图如下: image.png 其中主要有三个角色:

  1. 客户(client):客户类提出创建对象的请求
  2. 抽象原型(Prototype):规定拷贝接口
  3. 具体原型(ConcreatePrototyoe):被拷贝对象

原型模式通用写法

public interface Prototype<T> {
    Prototype<T> clone();
}
public class ConcretePrototypeB implements Prototype{

    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public ConcretePrototypeB clone() {
        ConcretePrototypeB concretePrototypeB = new ConcretePrototypeB();
        concretePrototypeB.setName(this.name);
        concretePrototypeB.setAge(this.age);
        return concretePrototypeB;
    }

    @Override
    public String toString() {
        return "ConcretePrototypeB{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}
public class Test {
    public static void main(String[] args) {
        ConcretePrototypeB concretePrototypeB = new ConcretePrototypeB();
        concretePrototypeB.setName("张三");
        concretePrototypeB.setAge(19);
        ConcretePrototypeB concretePrototypeB_copy = concretePrototypeB.clone();

        System.out.println(concretePrototypeB);
        System.out.println(concretePrototypeB_copy);

    }
}

image.png 上述的例子就是最简单的原型模式,可能有些人会觉得在里面set值和在外面好像一样只不过将对象创建设置的方法移到里面clone方法里面了而已,话虽然是这么说,的确看到的也是这样,但是有些对象的变量是私有的外部是不能访问的,此时再外部进行实例化然后在进行复制那么必然一些私有的变量是没有办法复制的。所以还是有一点点差别的。

通过这个例子再说一下原型模式,用通俗点话来讲其实就是在你不是通过在外部使用new 关键字来创建对象而是使用拷贝对象调用内部克隆方法创建的类的模式就是原型模式

但是大部分的克隆并不是像上文中的实现方式一样,一般都是直接基于内存二进制流进行拷贝,无需在经历夯实的对象初始化过程(不调用构造函数),这样性能提升很多,当对象构建过程比较耗时是,可以利用当前系统中已存在的对象作为原型,对其进行克隆

原型模式实现改造(实现Cloneable接口)

public class ConcretePrototypeB implements Cloneable{

    private String name;
    private Integer age;

    private List<String> family;

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public List<String> getFamily() {
        return family;
    }

    public void setFamily(List<String> family) {
        this.family = family;
    }

    @Override
    public ConcretePrototypeB clone() {
        ConcretePrototypeB clone = null;
        try {
            clone = (ConcretePrototypeB) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
        return clone;
    }

    @Override
    public String toString() {
        return "ConcretePrototypeB{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}

只需要实现Cloneable接口实现以下clone方法即可这样对于很复杂的对象赋值其实就很简单了

不过这里也有个小问题如下:

public class Test {
    public static void main(String[] args) {
        ConcretePrototypeB concretePrototypeB = new ConcretePrototypeB();
        concretePrototypeB.setName("张三");
        concretePrototypeB.setAge(19);
        List<String> family = new ArrayList<>();
        family.add("爷爷");
        family.add("奶奶");
        family.add("爸爸");
        family.add("妈妈");
        family.add("妹妹");
        concretePrototypeB.setFamily(family);
        ConcretePrototypeB concretePrototypeB_copy = concretePrototypeB.clone();
        concretePrototypeB_copy.setName("李四");
        concretePrototypeB_copy.getFamily().add("外公");
        concretePrototypeB_copy.getFamily().add("外婆");
        System.out.println("张三" + concretePrototypeB);
        System.out.println("李四" + concretePrototypeB_copy);
    }
}

image.png 上图中的打印结果可以知道Cloneable接口实现的克隆是浅拷贝,所以我们在该其他对象的时候连带所有的对象都改了

浅拷贝:赋值对象的引用地址,如下图所示: image.png 两个指针指向了同一片地址,所以一个修改所有的都修改了

使用序列化实现深度克隆

基于上述的问题我们可以再改造一下添加deepClone方法如下代码

public ConcretePrototypeB deepClone() {
    try {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(out);
        oos.writeObject(this);
        ByteArrayInputStream inputStream = new ByteArrayInputStream(out.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(inputStream);
        ConcretePrototypeB deepClone = (ConcretePrototypeB)ois.readObject();
        out.close();
        oos.close();
        inputStream.close();
        ois.close();
        return deepClone;
    }catch (Exception e){
        e.printStackTrace();
    }
    return null;
}

image.png

原型模式在源码中的应用

image.png image.png

原型模式的优缺点以及应用场景

优点:

  1. 性能优良,Java自带的原型模式是基于内存二进制流的拷贝,比直接new一个对象性能上提升了很多
  2. 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用

缺点:

  1. 需要为每个类配置克隆方法
  2. 克隆方法位于类的内部,当对已有类进行改造的时候,需要修改克隆代码,违背开闭原则
  3. 实现深度克隆的时候代码复杂如果设计嵌套类更麻烦

适用场景:

  1. 类初始化消耗资源较多的情况
  2. new的过程很复杂
  3. 构造函数很复杂
  4. 需要循环产生大量对象的时候

我们平常使用的BeanUtils就是原型模式