首页 > 技术文章 > 设计模式(四):原型模式

helianxiaowu 2021-09-26 18:55 原文

什么是原型模式?为什么要使用原型模式?

前两天面试了一个95年硕士毕业的小姐姐,在杭州某大厂工作了两年,最近想回家乡发展

对于两年以上工作经验的候选人,我都会问一些和设计模式相关的面试题

不得不面对一个现实,大部分候选人对设计模式都没有很深入的理解,回答的并不出彩

当我对这个小姐姐提出这两个问题时,也没抱有很高的期望。没想到小姐姐的回答很让人意外,甚至可以说是让我对原型模式有了更深刻的理解

为什么要使用原型模式

假如有一个类,命名为 AA 类里面有两个属性,分别是 xy ,并为这两个属性提供对应的 getset 方法

将这个类的实体对象 a 作为 test 方法的参数

要求在 test 方法内利用 a 对象的某些属性进行一些业务逻辑处理,但不能改变 a 对象的原有属性

我们进行第一次尝试:声明一个新的对象 a1 ,并把 a 赋值给它。让 test 方法利用 a1 对象的属性进行业务逻辑处理

public static void test(A a) {
 A a1 = a;
    System.out.println("test方法开始业务逻辑处理");
 a1.setX(1);
}

我们来验证一下是否会影响到 a 对象的属性

public static void main(String[] args) {
 A a = new A();
 a.setX(0);
 System.out.println("调用test方法前x=" + a.getX());
 test(a);
 System.out.println("调用test方法后x=" + a.getX());
}

输出结果为

从输出结果来看,test 方法改变了 a 对象的属性,不符合要求。所以,第一次尝试失败

其实也不难理解,我们都知道 JVM 加载对象后会给对象分配内存空间

加载完 a 之后,给 a 分配一个空间

在加载a1 的时候,因为 a1 是将 a 的值赋值给了 a1 ,所以在给 a1 分配空间时,只是把 a1 的引用指向了 a 所在的内存地址,并没有给 a1 分配独立的内存空间

所以修改 a1 对象的属性时,a 对象也会被改变

我们调整思路进行第二次尝试:重新 new 一个新对象 a2 ,把 a 对象的所有属性值赋值给 a2test 方法利用 a2 对象进行业务逻辑处理

public static void test(A a) {
 A a2 = new A();
 a2.setX(a.getX());
 a2.setY(a.getY());
 System.out.println("test方法开始业务逻辑处理");
 a2.setX(1);
    a2.setY(2);
}

同样来验证一下是否会影响到 a 对象的属性

public static void main(String[] args) {
 A a = new A();
 a.setX(0);
 a.setY(0);
 System.out.println("调用test方法前x=" + a.getX() + ",y=" + a.getY());
 test(a);
 System.out.println("调用test方法后x=" + a.getX() + ",y=" + a.getY());
}

输出结果为

这次的输出结果显示,test 方法并没有改变 a 对象的属性,符合要求

但是,有一个问题

  • 如果 a 不是一个具体的实例,而是一个抽象类或者接口。抽象类或者接口是不能被 new 的,该怎么办?

这时候就要使用到 原型模式 来解决我们的问题了

原型模式

原型模式定义

「原型模式」可以让你复制或克隆一个已有对象,而又无需使你的代码依赖这个对象所属的类

通过定义我们可以提取出来两个关键信息

第一,原型模式主要作用是复制或克隆一个已有对象

第二,去复制这个对象时不需要依赖这个对象所属的类

这句话很有意思,想要创建一个对象但是不用依赖这个对象所属的类,这要怎么实现?

答案就是把创建对象的过程交给这个类来处理

原型模式实战

我们用原型模式来优化一下上面的例子

动手之前我们需要知道原型模式的设计思路

根据定义可以知道在原型模式中,对象的创建过程是交给对象所属的类来处理的,所以这个类肯定要提供一个方法,方法的返回值是这个对象。通常这个方法叫 clone()copy()

套用到上面例子的 A 类中,需要在 A 类里面提供一个 clone() 方法,在方法中创建一个当前对象并返回

test 方法中利用 clone() 来获取一个 a3 对象去执行业务逻辑

public static void test(A a) {
 A a3 = a.clone();
 System.out.println("test方法开始业务逻辑处理");
 a3.setX(1);
}

再验证一下是否改变了 a 对象的属性

从输出结果可以看到是没有改变 a 对象的属性的

那我们再来解决上面例子中遇到的问题,假如 A 是一个抽象类,该怎么去创建这个对象

其实也很简单,抽象类中是可以有抽象方法的。把 clone() 方法定义为抽象方法,让子类去实现它

假如 A 有两个子类,分别是 SubA1SubA2。两个子类分别继承 A 抽象类,并实现 clone()抽象方法

test 中还是使用 a.clone() 就可以得到一个新的对象,而且不会影响到原有的 a 对象

这就用 原型模式 对上面的例子完成了优化

深拷贝、浅拷贝

在java中,默认 Object 类是所有类的父类,在 Object 中有一个 clone() 方法

它是java默认提供的用来复制对象的方法,这个方法是 native 修饰的,说明它是对操作系统的底层直接调用的,在理论上,用它来复制对象性能会更好

所以,我们可以使用 java.lang.Object#clone() 来实现原型模式,总共分为两步

  1. 被复制的类需要实现 Cloneable 这个接口类。这个接口类里面是没有任何一个方法的,只是起到一个标记作用,也可以理解成一种 约定 (「约定大于配置」)
  2. 被复制的类需要重写 Object 中的 clone() 方法

下面我们就来优化一下 A

这样写出的原型模式,在理论上执行效率更高。看似完美,实则不然

假如 A 类里面有一个 ArrayList 属性

我们来看一下,在 clonea 后得到 a4,改变 a4list 属性,会不会对 a 造成影响

输出结果为

在修改 a4 对象时,也改变了 a 对象的属性值,这不是我们期望的结果

这是因为:Objectclone 时只会对基础类型的数据进行拷贝,引用类型的数据并没有真正的拷贝,而是把引用指针指向了这个数据在内存中的地址(还记得上文中 aa1 指向同一个内存地址的例子吗)

这种只拷贝基础数据类型的行为,我们称之为 浅拷贝。既可以拷贝基础数据类型,又可以拷贝引用数据类型的行为,我们称之为深拷贝

在原型模式中,我们应该使用 深拷贝 来复制对象。

要实现深拷贝,「需要这个引用类型的数据所属的类也实现 Cloneable 接口,并且重写 Object 类的 clone() 方法」

在本例子中,引用类型所属的类是 ArrayList,它本身已经实现了 Cloneable 接口,并重写了 Object 类的 clone() 方法。

我们只需要在 A 类的 clone() 方法中调用 ArrayListclone() 方法即可

这样就基于 深拷贝 完成了原型模式

总结

「原型模式」也叫「克隆模式」,它属于设计模式三大类型中的创建型模式

在你需要复制一个对象,而又不希望改变原有对象的时候可以考虑使用原型模式来实现

在实现原型模式时,引用类型数据的复制要基于 深拷贝 ,否则会影响到被拷贝的 原型

Spring 生态下,对象的创建基本都由 IOC 来实现,原型模式 好像没有多少用武之地

但是,用的少不代表没用。我们在学习设计模式时,目的不仅仅在于要学会设计模式,而是要学会设计模式使用的设计思想

学会这种思想,沉淀为自己的思路,在工作中能实现举一反三,才能无往不利

-- 以上内容来自公众号 「赫连小伍」 ,转载请注明出处

推荐阅读