你一定要知道的设计模式-原型模式

原型模式

原型模式是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,数据创建型模式。

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

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

客户(Client):客户类提出创建对象的请求。

抽象原型(Prototype):规定拷贝接口。

具体原型(Concrete Prototype):被拷贝的对象。

原型模式的应用场景

你一定遇到过大篇幅,get,set复制的场景。例如这样的代码:

User user = new User();
user.setName(userVo.getName());
user.setAge(userVo.getAge());
...
复制代码

代码非常工整,命名非常规范,但是这样的代码优雅么?这就是纯体力活,如果一个实体类字段有二十个,那你就需要get,set二十次,那么原型模式,能帮助我们解决这样的问题。

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

  1. 类初始化消耗资源较多
  2. new产生的一个对象需要非常繁琐的过程(数据猪呢比,访问权限等)
  3. 构造函数比较复杂
  4. 循环体中产生大量对象时

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

原型模式的通用写法

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

public class ConcretePrototype implements IPrototype{
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

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

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

    public static void main(String[] args) {
        //创建原型对象
        ConcretePrototype concretePrototype = new ConcretePrototype();
        concretePrototype.setAge(18);
        concretePrototype.setName("lihaha");
        System.out.println(concretePrototype);
        //拷贝对象
        ConcretePrototype cloneType = concretePrototype.clone();
        System.out.println(cloneType);
    }
}
复制代码

结果:

image.png

像我们项目中,跟前端打交道的是VO,需要把DTO的数据转换成VO,但是有一部分数据不能给用户看到。这时候,如果正常流程应该是:

UserVo userVo = new UserVo();
userVo.setName(userDto.getName());
userVo.setAge(userDto.getAge());
//这里UserDto还有很多属性,比如密码等信息是不能给前端返回的,所以就不会给VO赋值
复制代码

这里insert,update,等都需要调用,调用非常的多,也可能其他地方也会调用,每个都要写一遍真的累死了

原型模式改造:

UserDto:

public class UserDto implements IPrototype{

    private String name;
    private int age;
    private String password;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

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

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

    public static void main(String[] args) {
        UserDto userDto = new UserDto();
        userDto.setAge(1);
        userDto.setName("lihaha");
        userDto.setPassword("123");
        System.out.println(userDto.toString());
        UserVo userVo = userDto.clone();
        System.out.println(userVo.toString());
    }
}
复制代码

UserVo:

public class UserVo {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "UserVo{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
复制代码

结果:

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

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

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

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

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

    public static void main(String[] args) {
        //创建原型对象
        ConcretePrototype concretePrototype = new ConcretePrototype();
        concretePrototype.setAge(18);
        concretePrototype.setName("lihaha");
        System.out.println(concretePrototype);
        //拷贝对象
        ConcretePrototype cloneType = concretePrototype.clone();
        System.out.println(cloneType);
    }
}
复制代码

结果:

image.png
我们来改造一下代码:

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

    public List<String> getHobbies() {
        return hobbies;
    }

    public void setHobbies(List<String> hobbies) {
        this.hobbies = hobbies;
    }

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

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

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

    public static void main(String[] args) {
        //创建原型对象
        ConcretePrototype concretePrototype = new ConcretePrototype();
        concretePrototype.setAge(18);
        concretePrototype.setName("lihaha");
        List<String> hobbies = new ArrayList<>();
        hobbies.add("书法");
        hobbies.add("美术");
        concretePrototype.setHobbies(hobbies);
        System.out.println(concretePrototype);
        //拷贝对象
        ConcretePrototype cloneType = concretePrototype.clone();
        cloneType.getHobbies().add("技术");
        System.out.println("原型:"+concretePrototype);
        System.out.println("克隆:"+cloneType);
    }
}
复制代码

结果:

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

使用序列化实现深克隆

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

    public List<String> getHobbies() {
        return hobbies;
    }

    public void setHobbies(List<String> hobbies) {
        this.hobbies = hobbies;
    }

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

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

    public ConcretePrototype deepClone() {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            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;
        }
    }

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

    public static void main(String[] args) {
        //创建原型对象
        ConcretePrototype concretePrototype = new ConcretePrototype();
        concretePrototype.setAge(18);
        concretePrototype.setName("lihaha");
        List<String> hobbies = new ArrayList<>();
        hobbies.add("书法");
        hobbies.add("美术");
        concretePrototype.setHobbies(hobbies);
        System.out.println(concretePrototype);
        //拷贝对象
        ConcretePrototype cloneType = concretePrototype.deepClone();
        cloneType.getHobbies().add("技术");
        System.out.println("原型:"+concretePrototype);
        System.out.println("克隆:"+cloneType);
    }
}
复制代码

结果:
image.png

克隆破坏单例模式

如果我们的克隆的目标的对象是单例对象,那意味着,深克隆就会破坏单例。实际上防止克隆破坏单利解决思路很简单,禁止深度克隆就可以。要么单例类不实现Cloneable接口,要么我们重写clone()方法,在clone方法中返回单例对象即可,具体代码如下

protected object clone() throws CloneNotSupportedException{
    return INSTANCE;
}
复制代码

原型模式在JDK源码中的应用

先来JDK中的Cloneable接口:

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 {
}
复制代码

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

public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
复制代码

原型模式的优缺点:

优点:

  1. 性能优良,Java自带的是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。

  2. 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。

缺点:

  1. 需要为每一个类型配置一个克隆方法

  2. 克隆方法位于类的内部,当对已有的类进行改造的时候需要修改代码,违反了开闭原则。

  3. 在实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深拷贝,浅拷贝需要运用得当。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享