设计模式(四):原型模式

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

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

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

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

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

为什么要使用原型模式

假如有一个类,命名为A。A类里面有两个属性,分别是x和y,并为这两个属性提供对应的get和set方法

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

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

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

publicstaticvoidtest(A a){

A a1 = a;

System.out.println("test方法开始业务逻辑处理");

a1.setX(1);

}

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

publicstaticvoidmain(String[] args){

A a =newA();

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对象的所有属性值赋值给a2。test方法利用a2对象进行业务逻辑处理

publicstaticvoidtest(A a){

A a2 =newA();

a2.setX(a.getX());

a2.setY(a.getY());

System.out.println("test方法开始业务逻辑处理");

a2.setX(1);

a2.setY(2);

}

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

publicstaticvoidmain(String[] args){

A a =newA();

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对象去执行业务逻辑

publicstaticvoidtest(A a){

A a3 = a.clone();

System.out.println("test方法开始业务逻辑处理");

a3.setX(1);

}

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

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

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

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

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

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

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

深拷贝、浅拷贝

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

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

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

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

被复制的类需要重写Object中的clone()方法

下面我们就来优化一下A类

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

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

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

输出结果为

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

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

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

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

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

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

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

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

总结

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

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

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

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容