设计模式-原型模式学习之旅

793 阅读5分钟

“这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

一、原型模式的定义

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

原型模式的核心在于拷贝原型对象。以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能提升许多。当对象的构建过程比较耗时时,可以利用当前系统中已存在的对象作为原型,对其进行克隆(一般是基于二进制流的复制),躲避初始化过程,使得新对象的创建时间大大减少。

原型模式主要包含三个角色:

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

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

二、原型模式的应用场景

你一定遇到过大篇幅getter、setter赋值的场景。(大多的是体力劳动~~~)

1. 原型模式主要适用于以下场景

  • 类初始化消耗资源较多。
  • new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)
  • 构造函数比较复杂。
  • 循环体中生产大量对象时。

在Spring中,原型模式应用得非常广泛。例如scope="prototype",在我们经常用的JSON.parseObject()也是一种原型模式。

2. 原型模式的通用写法

一个标准的原型模式代码,应该是这样设计的。先创建原型IPrototype接口:

public interface IPrototype<T> {
    T clone();
}

创建具体需要克隆的对象ConcretePrototype:

@Data
public class ConcretePrototype implements Cloneable {
    private String name;
    private int age;

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

测试代码:

public class PrototypeTest {

    @Test
    public void test() {
        //创建原型对象
        ConcretePrototype concretePrototype = new ConcretePrototype();
        concretePrototype.setName("Mark");
        concretePrototype.setAge(18);
        System.out.println(concretePrototype);

        //拷贝原型对象
        ConcretePrototype cloneType = concretePrototype.clone();
        System.out.println(cloneType);
    }
}

运行结果:

image.png

这时候,有小伙伴就问了,原型模式就这么简单么?对,就是这么简单。在这个简单的场景之下,看上去操作好像变复杂了。但如果有几百个属性需要复制,那我们就可以一劳永逸。但是,上面的复制过程是我们自己完成的,在实际编码中,我们一般不会浪费这样的体力劳动,JDK已经帮我们实现了一个现成的API,我们只需要实现Cloneable接口即可。来改造一下代码,修改ConcretePrototype类:

@Data
public class ConcretePrototype implements Cloneable {
    private String name;
    private int age;

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

重新运行,也会得到同样的结果。有了JDK的支持再多的属性复制我们也能轻而易举地搞定了。下面我们再来一个测试,给ConcretePrototype类增加一个个人爱好的属性hobbies:

@Data
public class ConcretePrototype implements Cloneable {
    private String name;
    private int age;
    private List<String> hobbies;

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

修改客户端测试代码:

public class PrototypeTest {

    @Test
    public void test() {
        //创建原型对象
        ConcretePrototype concretePrototype = new ConcretePrototype();
        concretePrototype.setName("Mark");
        concretePrototype.setAge(18);
        List<String> hobbies = new ArrayList<>();
        hobbies.add("basketball");
        hobbies.add("coding");
        concretePrototype.setHobbies(hobbies);

        //拷贝原型对象
        ConcretePrototype cloneType = concretePrototype.clone();
        cloneType.getHobbies().add("swing");
        System.out.println("原型对象:" + concretePrototype);
        System.out.println("克隆对象:" + cloneType);
    }
}

运行结果:

image.png

我们给复制后的克隆对象新增一项爱好后,发现原型对象也发生了变化,这显然不符合我们的预期。因为我们希望克隆出来的对象应该和原型对象是两个独立的对象,不应该再有联系了。从测试结果分析来看,应该是hobbies共用了一个内存地址,意味着复制的不是值,而是引用的地址。这样的话,如果我们修改任意一个对象中的属性值,prototype和cloneType的bobbies值都会改变。这就是我们常说的浅克隆。 只是完整复制了值类型数据,没有复制引用对象。换言之,所有的引用对象仍然指向原来的对象,显然不是我们想要的结果。那如何解决这个问题呢?下面我们来看深度克隆继续改造。

3. 使用序列化实现深度克隆

在上边的代码基础上我们继续改造,来看代码,增加一个deepClone()方法:

@Data
public class ConcretePrototype implements Cloneable, Serializable {
    private String name;
    private int age;
    private List<String> hobbies;

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

    public ConcretePrototype deepClone() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);

            return (ConcretePrototype) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }    
}

来看客户端调用代码:

@Data
public class ConcretePrototype implements Cloneable, Serializable {
    private String name;
    private int age;
    private List<String> hobbies;

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

    public ConcretePrototype deepClone() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);

            return (ConcretePrototype) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }    
}

运行结果如下:

image.png

三、原型模式在源码中的应用

先来看JDK中的Cloneable接口:

public interface Cloneable {
}

接口定义还是很简单的,我们找到源码其实只需要找到看哪些接口实现了Cloneable接口即可。来看ArrayList类的实现。

image.png

我们发现方法中只是将List中的元素循环遍历了一遍。这个时候我们再思考一下,是不是这种形式就是深克隆呢?其实用代码验证一下就知道了,继续修改ConcretePrototype类,增加一个deepCloneHobbies()方法:

public ConcretePrototype deepCloneHobbies() {
    try {
        ConcretePrototype result = (ConcretePrototype) super.clone();
        result.setHobbies((List) ((ArrayList) result.getHobbies()).clone());
        return result;
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
        return null;
    }
}

修改客户端代码:

public class PrototypeTest {

    @Test
    public void test() {
        //创建原型对象
        ConcretePrototype concretePrototype = new ConcretePrototype();
        concretePrototype.setName("Mark");
        concretePrototype.setAge(18);
        List<String> hobbies = new ArrayList<>();
        hobbies.add("basketball");
        hobbies.add("coding");
        concretePrototype.setHobbies(hobbies);

        //拷贝原型对象
        ConcretePrototype cloneType = concretePrototype.deepCloneHobbies();
        cloneType.getHobbies().add("swing");
        System.out.println("原型对象:" + concretePrototype);
        System.out.println("克隆对象:" + cloneType);
        System.out.println(concretePrototype == cloneType);
    }
}

运行结果如下:

image.png

运行也能得到期望的结果。但是这样的代码,其实都是硬编码,如果在对象中声明了各种集合类型,那每种情况都需要单独处理。因此,深克隆的写法,一般会直接用序列化来操作。

四、原型模式的优缺点

优点:

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

缺点:

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

五、友情链接

设计模式-工厂模式学习之旅

设计模式-单例模式学习之旅

欢迎大家关注微信公众号(MarkZoe)互相学习、互相交流。