源码分析设计模式之原型模式

一、原型模式简介

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式

这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如:一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用

原型模式实际上就是从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节。在初始化的信息不发生变化的情况下,克隆是最好的办法,既隐藏了对象创建的细节,又大大提高了性能。因为如果不用 clone,每次 new 都需要执行一次构造函数,如果构造函数的执行时间很长,那么多次的执行初始化操作就太低效了

原型模式的重点在从自身赋值自己创建新的类对象,隐藏创建的细节

二、原型模式角色

  • 抽象原型(Prototype)角色:规定了具体原型对象必须实现的接口(如果要提供深拷贝,则必须具有实现 clone 的规定)
  • 具体原型(ConcretePrototype):从抽象原型派生而来,是客户程序使用的对象,即被复制的对象,需要实现抽象原型角色所要求的接口
  • 客户(Client)角色:使用原型对象的客户程序
/* ----------------------- v1 版本的服务端 ----------------------- */
@Setter
@Getter
@ToString
public class DouYinVedio implements Cloneable {
    
    
    private String name;
    private Date publishTime;
	
    @Override
    protected Object clone() throws CloneNotSupportedException {
    
    
        return super.clone();
    }
	
    public DouYinVedio() {
    
    }

    public DouYinVedio(String name, Date publishTime) {
    
    
        this.name = name;
        this.publishTime = publishTime;
    }
}
/* ----------------------- v1 版本的服务端 ----------------------- */
/* ----------------------- v1 版本的客户端 ----------------------- */
public class Client {
    
    
    public static void main(String[] args) throws CloneNotSupportedException {
    
    
        Date date = new Date();
        DouYinVedio v1 = new DouYinVedio("鞠婧祎", date);
        System.out.println("v1 ==> " + v1);
        System.out.println("v1的hashcode值 ==>" + v1.hashCode());
        // 现在开始以 V1原型对象 克隆 杂版v2
        DouYinVedio v2 = (DouYinVedio) v1.clone();
        System.out.println("v2 ==> " + v2);
        System.out.println("v2的hashcode值 ==>" + v2.hashCode());
        //  这个时候克隆出来的对象和原来是一摸一样的,但是确是两个对象
    }
}
/* ----------------------- v1 版本的客户端 ----------------------- */

鞠婧祎

这个时候我们对客户端进行变化,基于 v1 版本服务端的 v2 版本客户端
/* ----------------------- 基于 v1 版本服务端的 v2 版本客户端 ----------------------- */
public class Client {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Date date = new Date();
        DouYinVedio v1 = new DouYinVedio("鞠婧祎", date);
        DouYinVedio v2 = (DouYinVedio) v1.clone();
        System.out.println("v1 ==> " + v1);
        System.out.println("v2 ==> " + v2);
        System.out.println("变化来了 ==================================");
        date.setTime(13213151);		// 改变时间
        System.out.println("v1 ==> " + v1);
        System.out.println("v2 ==> " + v2);
    }
}
/* ----------------------- 基于 v1 版本服务端的 v2 版本客户端 ----------------------- */

鞠婧祎

这个时候我们会发现,当我们改变了时间,v2 也会随之改变,这就是浅拷贝
  • 浅拷贝
    被拷贝对象的所有变量都含有与原对象相同的值,而且对其他对象的引用仍然是指向原来的对象。即:浅拷贝只负责当前对象实例,对于对象中的引用类型对象不做拷贝。换句话说就是浅拷贝只拷贝对象中的基本数据类型,对于容器、数组之类的引用对象不会进行拷贝拷贝
  • 深拷贝
    被拷贝对象的所有的变量都含有与原来对象相同的值,除了引用其他对象的变量。引用其他对象的变量将指向一个被拷贝的新对象,而不再是原有被引用对象。即:深拷贝把要拷贝的对象所引用的对象也都拷贝了一次。换句话说就是深拷贝不仅能够拷贝对象中的基本数据类型,还能拷贝数组、容器之类的引用对象
    深拷贝要深入到多少层,是一个不确定的问题。在决定以深拷贝的方式拷贝一个对象的时候,必须决定对间接拷贝的对象是采取浅拷贝还是深拷贝还是继续采用深拷贝。因此,在采取深拷贝时,需要决定多深才算深。此外,在深拷贝的过程中,很可能会出现循环引用的问题
实现深拷贝的方式,基于 v2 版本客户端的 v2 版本服务端
/* ----------------------- 基于 v2 版本客户端的 v2 版本服务端 ----------------------- */
@Setter
@Getter
@ToString
public class DouYinVedio implements Cloneable {
    
    
    private String name;
    private Date publishTime; // 发布时间
	
    @Override
    protected Object clone() throws CloneNotSupportedException {
    
    
       	DouYinVedio vedio = (DouYinVedio) super.clone();
       	vedio.publishTime = (Date) this.publishTime.clone();	// 将这个对象的属性也进行克隆
    	return vedio;
    }
	
    public DouYinVedio() {
    
    }
	
    public DouYinVedio(String name, Date publishTime) {
    
    
        this.name = name;
        this.publishTime = publishTime;
    }
}
/* ----------------------- 基于 v2 版本客户端的 v2 版本服务端 ----------------------- */

鞠婧祎
由于 Date 不是基本数据类型,所以成员变量 publishTime 不会被拷贝,需要我们自己实现深拷贝。而幸运的是 Java 中提供的大部分的引用类型都实现了 Cloneable 接口,所以实现深拷贝并不是特别困难

这种方式的拷贝是在内存层面,我们也可以使用序列化与反序列化的方式来实现深拷贝,但是因为序列化与反序列化是与 IO 打交道,这样的效率就会降低很多,拷贝在内存层面的效率最高
原型模式的注意事项
  • 原型模式复制对象不会调用类的构造方法。因为对象的复制是通过调用 Object 类中的 clone 方法来完成的,它直接在内存中复制数据,因此不会调用到类的构造方法。不但构造方法中的代码不会执行,甚至连访问权限都对原型模式无效。就好比单例模式,在单例模式中,只要将构造方法的访问权限设置为 private 型,就可以实现单例。但是 clone 方法直接无视构造方法的权限,所以单例模式与原型模式是冲突的,在使用时要特别注意
  • Object 类中的 clone 方法只会拷贝对象中的基本的数据类型,对于数组、容器对象之类的引用对象都不会进行拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象之类的引用对象另行拷贝
原型模式的优点
  • 使用原型模式来创建对象比直接 new 对象在性能上要好的多,因为 Object 类中的 clone 方法是一个本地方法,它的工作原理是:从堆区以二进制流的方式进行拷贝,直接分配一块新内存。特别是复制大对象时,性能的差别非常明显
  • 使用原型模式可以简化对象的创建过程,使得创建对象就像我们在编辑文档时的复制粘贴一样简单
  • 可以使用深拷贝的方式来保存对象的状态
原型模式的缺点
  • 需要为每一个类实现 Cloneable 接口和配备一个克隆方法,这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难,但对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,这样就违背了设计原则中的开封原则,而且当内部包括一些不支持拷贝或有循环引用的对象时,实现克隆可能也会很困难的,就好比 Thread、Socket 对象不是简单直接就可以被克隆
  • 在实现深克隆时需要编写较为复杂的代码
原型模式的使用场景

基于原型模式的优点,所以在需要重复地创建相似对象时可以考虑使用原型模式。比如需要在一个循环体内创建对象,假如对象创建过程比较复杂或者循环次数很多的话,使用原型模式不但可以简化创建过程,而且可以使系统的整体性能提高很多

猜你喜欢

转载自blog.csdn.net/qq_39249094/article/details/117638572