1.原型模式概述

原型模式是一种非常简单易懂的模型,在书上的定义是这样的:

通俗的讲,就是有一个现成的对象,我们通过复制或拷贝这个对象的方式来创建一个新的对象,这就是原型模式。


那么,我们为什么需要通过拷贝来创建对象呢?
我认为主要体现在两个维度:程序运行效率开发效率

  • 程序运行效率:如果对象的创建和初始化的代价比较大,创建比较繁琐,例如需要从数据库、RPC、网络等获取一些数据才能完成创建,这时候使用原型模式拷贝出一个对象无疑是效率更高的做法。
  • 开发效率:我们在业务开发中,会涉及到不同的分层,每个分层都有自己的数据实例,例如与前端交互的VO对象,数据传输的DTO对象,与数据库交互的PO对象等等,这些对象转换的过程中大部分的字段值都是相同的,如果每一层都使用getter/setter方法来进行赋值,开发效率就很容易受到影响,尤其是在迭代中增加了新字段的情况下,每一层都需要新增一个字段set方法,很麻烦也很容易做漏,这时候使用原型模式来进行复制就比较方便了。

严格的说,原型模式是同一个类型下的不同实例的数据拷贝,对于不同类型的实例拷贝更应该归类于原型模式的一种拓展用法。不过在业务中,这种拓展用法使用的频率更高,接下来的示例也是以这种拓展用法为主。

2.浅拷贝与深拷贝

想要在使用原型模式的时候,不出现一些“意外”,那就得先了解浅拷贝与深拷贝之间的区别

两者的区分非常好理解,关键点就是在对引用类型的成员变量拷贝上,两种拷贝类型有不同的结果:

  • 浅拷贝:只会将原始对象中的引用类型变量的内存地址复制给目标对象的同名成员变量。
  • 深拷贝:会以引用类型变量的类型为基础,创建一个崭新的对象,再把这个新对象的内存地址赋值给目标对象的对应同名成员变量。

对于浅拷贝来说,如果原始对象中有其他的引用类型变量,在拷贝出目标对象后,两个对象对于该引用类型变量的修改会互相影响,但是由于不需要针对引用类型的变量查询新的对象,这种拷贝方式的效率较高。在不会修改变量值或者只需要修改基本类型的变量值(包含String)时,优先考虑使用浅拷贝。

如果拷贝的对象中包含了引用类型的对象,且需要进行修改,或者拷贝不同类型但内部字段相同的对象(例如:UserPO和UserDTO),可以考虑使用深拷贝。

2.1.浅拷贝的实现方式

浅拷贝的实现方式有很多,归类起来主要是两种,一种是使用Java原生的方式,另一种是通过开源的工具包实现。


先看看Java原生的方式,实现一个Cloneable接口,并覆写父类Object中的clone方法:

@Getter
@Setter
public class User implements Cloneable {
    private String name;
    private int age;
    private Phone phone;

    @Override
    public User clone() throws CloneNotSupportedException {
        return (User) super.clone();
    }
}

@Getter
@Setter
public class Phone {
    private String phoneNumber;
}

为了验证浅拷贝,这里还加入了一个Phone对象,接下来就做个测试。

public static void main(String[] args) throws CloneNotSupportedException {
    User user = new User();
    user.setName("张三");
    user.setAge(18);
    user.setPhone(new Phone());

    User cloneUser = user.clone();
    System.out.println(user.getPhone() == cloneUser.getPhone());
}

打上一个断点,查看两个对象,可以看到User对象已经成功复制,并且里面的Phone对象明显是同一个对象,
【设计模式】使用原型模式完成业务中“各种O”的转换-LMLPHP


使用开源的工具,常用的有两种工具分别是Apache commons中的BeanUtilsPropertyUtilsSpring中的BeanUtils,他们都有一个共同的方法是copyProperties用来拷贝对象属性。在实现中,虽然ApacheSpring都是通过反射来实现的,但是Spring针对反射做了一层缓存,在相同类型的对象复制中,效率高于Apache,所以我们选择使用Spring的拷贝。

没有Spring依赖的话,需要引入依赖包:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.3.9</version>
</dependency>

修改一下代码,可以得到一样的结果:

public static void main(String[] args) {
    User user = new User();
    user.setName("张三");
    user.setAge(18);
    user.setPhone(new Phone());

    User cloneUser = new User();
    BeanUtils.copyProperties(user, cloneUser);
    System.out.println(user.getPhone() == cloneUser.getPhone());
}

【设计模式】使用原型模式完成业务中“各种O”的转换-LMLPHP

2.2.深拷贝的实现方式

深拷贝的实现有两种方式,一种是通过递归来拷贝,既然一次拷贝对于引用变量来说只能拷贝地址,那就再把引用变量也做一次拷贝就可以了,只是这种方式太麻烦了。我们一般会选择第二种方式,通过序列化反序列化来完成对象的拷贝。

其实在我们常见的RPC通信中,就是使用的这种方式来完成对象拷贝的,请求方将对象序列化成流、字节数组、JSON、XML等等方式来做传输,接收方收到数据后,用同样的方式反序列化成一个新的对象。我们也可以采用这种方式来完成对象的拷贝。


这里使用一个FastJson工具类做了一个简单的封装,封装了两个方法,拷贝单个对象和拷贝List对象:

public class DeepCloneUtil {

    /**
     * 克隆单个对象
     *
     * @param source      被克隆的源对象
     * @param targetClazz 克隆目标对象的类型
     * @param <T>         目标对象泛型
     * @return 克隆模板对象
     */
    public static <T> T cloneObject(Object source, Class<T> targetClazz) {
        String jsonString = JSON.toJSONString(source);
        return JSON.parseObject(jsonString, targetClazz);
    }

    /**
     * 克隆List对象
     *
     * @param source      被克隆的源对象
     * @param targetClazz 克隆目标对象的类型
     * @param <T>         目标对象泛型
     * @return 克隆模板对象
     */
    public static <T> List<T> cloneList(List<?> source, Class<T> targetClazz) {
        String jsonString = JSON.toJSONString(source);
        return JSON.parseArray(jsonString, targetClazz);
    }

}

修改一下测试代码:

public static void main(String[] args) {
    User user = new User();
    user.setName("张三");
    user.setAge(18);
    user.setPhone(new Phone());

    User cloneUser = DeepCloneUtil.cloneObject(user, User.class);
    System.out.println(user.getPhone() == cloneUser.getPhone());
}

【设计模式】使用原型模式完成业务中“各种O”的转换-LMLPHP
此时,两个Phone对象就不是同一个对象了,这样就完成了深拷贝。


再试试列表拷贝:

public static void main(String[] args) {
    User user = new User();
    user.setName("张三");
    user.setAge(18);
    user.setPhone(new Phone());

    User user1 = new User();
    user1.setName("李四");
    user1.setAge(19);
    user1.setPhone(new Phone());

    List<User> list = Arrays.asList(user, user1);
    List<User> cloneList = DeepCloneUtil.cloneList(list, User.class);

}

【设计模式】使用原型模式完成业务中“各种O”的转换-LMLPHP
可以看到不管是List,还是User,还是Phone,每一个都是不同的对象。

3.结语

本篇主要讲述的是原型模式的概念及其使用,并引出了深拷贝与浅拷贝的区别。
不管是从创建对象的性能上考虑,还是从开发效率上考虑,都可以在合适的时候选择使用原型模式拷贝对象的方式来替代从头开始创建一个新的对象。

10-09 15:42