本章用来介绍设计模式中的原型模式,该模式也是设计模式中较为简单、也是比较常见的一种设计模式。同样给出场景及相应代码层层推进的方式来学习原型模式。
一、场景
假设现在公司内部要开发一款周报提交系统。周报需要填写的表单设计如下:
比如第一周我们填写了一份表单如下:
现在过了第二周,我们再填写一份表单,如下:
仔细看这两份表单,有什么问题?是不是主要产生变化的,只有表单中被加粗的这三项(本周总结、下周计划、提交日期),其它的几项是不是都不会怎么变动?如果,我们每周填写周报都要填写全部的项,是不是显得很虎?所以,我们的需求是能够保留上周填写的周报,在其基础上进行修改。
下面开始进行代码的模拟及步步推进。
二、代码模拟
为了方便模式的讲解,我们用一个WeekReport
类,来模拟这份表单。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public class AppTest {
public static void main(String[] args) throws CloneNotSupportedException, InterruptedException { WeekReport reportFirst = new WeekReport("Chemlez", "IT部门", "看了一本书", "再看一本书", "Nothing", new Date()); WeekReport reportSecond = new WeekReport("Chemlez", "IT部门", "又看完了一本书", "看两本书", "Nothing", new Date(new Date().getTime() + 7 * 24 * 3600 * 1000)); System.out.println(reportFirst); System.out.println(reportSecond);
} }
class WeekReport { private static int id = 0; private String emp; private String summary; private String plain; private String suggestion; private Date date;
public WeekReport() { ++id; }
public WeekReport( String emp, String summary, String plain, String suggestion, Date date) { ++id; this.emp = emp; this.summary = summary; this.plain = plain; this.suggestion = suggestion; this.date = date; }
}
===输出=== WeekReport{name='Chemlez', dept='IT部门', summary='看了一本书', plain='再看一本书', suggestion='Nothing', date=Tue May 18 21:18:38 CST 2021} WeekReport{name='Chemlez', dept='IT部门', summary='又看完了一本书', plain='看两本书', suggestion='Nothing', date=Tue May 25 21:18:38 CST 2021}
|
我们看到第二周周报,尽管第二周周报的大部分内容与第一周周报内容一致,但是仍然需要重复设置。
此时,我们便可以通过使用”原型模式”来解决这个问题。
市容
2.1 代码一
现在,我需要保留上周需要填写的内容。从面向对象的角度出发,就是我们能够”克隆”出原来的对象,然后复用这个对象的属性,直接在”克隆”出的对象上进行修改我们需要修改的部分即可,不用修改的部分就不去改动它即可。
这里使用Java
中提供的接口,Cloneable
接口。
1 2
| public interface Cloneable { }
|
可以看到这个接口没有定义任何的方法,其实这只是一个标记接口,类似于Serializable
这个接口。只是告诉JVM
该类实例化的对象是可以被”克隆”的。
要想达到这个上面”克隆”的效果,我们需要重写该接口。即:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class WeekReport implements Cloneable { private static int id = 0; private String name; private String dept; private String summary; private String plain; private String suggestion; private Date date;
public WeekReport() { ++id; }
public WeekReport(String name, String dept, String summary, String plain, String suggestion, Date date) { ++id; this.name = name; this.dept = dept; this.summary = summary; this.plain = plain; this.suggestion = suggestion; this.date = date; }
@Override public Object clone() throws CloneNotSupportedException { return super.clone(); }
}
|
clone
方法是直接复制内存中的二进制(重新开辟的一块内容空间用来存储),效率更高。
这个时候我们就可以直接进行对象的克隆,使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class AppTest {
public static void main(String[] args) throws CloneNotSupportedException,InterruptedException {
WeekReport reportFirst = new WeekReport("Chemlez", "IT部门", "看了一本书", "再看一本书", "Nothing", new Date());
WeekReport reportSecond = (WeekReport) reportFirst.clone(); reportSecond.setSummary("又看了一本书"); reportSecond.setPlain("看两本书"); Date date = reportSecond.getDate(); date.setTime(date.getTime() + 7*24*3600*1000); System.out.println(reportFirst); System.out.println(reportSecond); } }
====输出==== WeekReport{name='Chemlez', dept='IT部门', summary='看了一本书', plain='再看一本书', suggestion='Nothing', date=Tue May 25 21:19:35 CST 2021} WeekReport{name='Chemlez', dept='IT部门', summary='又看了一本书', plain='看两本书', suggestion='Nothing', date=Tue May 25 21:19:35 CST 2021}
|
可以看出,我们将源对象进行克隆,在现有对象上修改我们想要修改(通过set和get方法)的属性即可,是不是就满足了我们所提出的需求?
不过细心的读着可能看出了,为什么我们的周报一的时间和周报二的时间是一样的了?难道克隆出来的对象和源对象指向同一个地址?
我们可以来测试一下,比较两个对象是否指向同一个地址。
1 2 3
| System.out.println(reportFirst==reportSecond); ===输出=== false
|
结果为false,所以可以看到两个对象并非指向同一个地址。其实,在上面我们也写到了,clone
方法是直接复制内存中的二进制(重新开辟的一块内容空间用来存储),所以两个对象并非指向同一片地址。而这种拷贝的方式是一种浅拷贝方式。请看下图:
一图胜千言。对象是两个对象,但是对象中的属性,如果是引用类型,那么在拷贝的时候就会指向同一片堆内存空间,也就是说他们的引用类型的属性指向的是同一个对象,所以改动其中一个属性也会改动另外一个对象的属性。
那么朝着这个目的,我们让其引用类型属性指向对象也是各自的对象,从而互不干扰。
2.2 代码二
我们改造的思路是,将其中的引用类型属性通过get
获取后,再进行clone
,然后再通过set
方法传回到原来clone
对象中。这么说有点绕,直接看代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class AppTest {
public static void main(String[] args) throws CloneNotSupportedException{
WeekReport reportFirst = new WeekReport("Chemlez", "IT部门", "看了一本书", "再看一本书", "Nothing", new Date());
WeekReport reportSecond = (WeekReport) reportFirst.clone(); Date date = reportSecond.getDate(); Date dateCloned = (Date) date.clone(); reportSecond.setDate(dateCloned);
reportSecond.setSummary("又读完了一本书"); reportSecond.setPlain("看两本书"); Date dateC = reportSecond.getDate(); dateC.setTime(dateC.getTime() + 7 * 24 * 3600 * 1000);
System.out.println(reportFirst); System.out.println(reportSecond); } }
====输出==== WeekReport{id=1, name='Chemlez', dept='IT部门', summary='看了一本书', plain='再看一本书', suggestion='Nothing', date=Tue May 18 21:31:29 CST 2021} WeekReport{id=1, name='Chemlez', dept='IT部门', summary='又读完了一本书', plain='看两本书', suggestion='Nothing', date=Tue May 25 21:31:29 CST 2021}
|
通过将date
进行克隆,返回的对象也是二进制复制,并且是新的内存空间。如下图:
一图胜千言。在代码:
1 2
| Date dateCloned = (Date) date.clone(); reportSecond.setDate(dateCloned);
|
第二句通过set,给reportSecond
重新设置了一个Date
对象,并且保证了,其内容值与原来的值一样,这样既能够保证我们可以复用原来的属性值,也可保证我们修改其属性,不会影响到过去的属性值。
到了这里,代码的改动似乎已经很完美了,但是肯定有读者有会产生这么一个疑问,你现在对象中只有一个引用类型的属性,如果我给你成百上千的引用类型的属性你该怎么办,难道还去一个一个clone么?如果你属性中引用类型的属性中中又存在引用类型的属性怎么办?层层嵌套的引用类型,用这种逐个属性克隆再赋值的方式,岂不是要累死。
因此,就需要对其继续进行改进。
3.3 代码三
代码二遗留下的来的问题是:如果一个类中包含的引用型对象过多,即如果对象的深度比较深,则深拷贝实现起来较为繁琐。
进一步的改进,我们还是需要实现Cloneable
接口,重写clone
克隆方法。即:在这里就需要修改WeekReport
的clone
方法。不过,在这里我们还需要将类去实现Serializable
接口。目的:是将对象进行序列化,然后再将对象进行反序列化,当反序列化为对象时,此时得到的对象就是一种天然的深拷贝方式得来的对象。
对象中无论存在多少的层级关系(引用关系),将这个对象序列化到硬盘上(二进制,序列化就是天生的深拷贝),在序列化的同时,也会将这种层级关系进行序列化。
实现方式一
根据上面的思路,现在我们将对象进行序列化,然后再将对象进行反序列化,从而达到深拷贝的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| public class AppTest {
public static void main(String[] args) throws CloneNotSupportedException {
WeekReport reportFirst = new WeekReport("Chemlez", "IT部门", "看了一本书", "再看一本书", "Nothing", new Date());
WeekReport reportSecond = (WeekReport) reportFirst.clone();
Date date = reportSecond.getDate(); date.setTime(date.getTime() + 7 * 24 * 3600 * 1000); reportSecond.setSummary("又对了一本书"); reportSecond.setPlain("再读两本书"); System.out.println(reportFirst); System.out.println(reportSecond);
} }
class WeekReport implements Cloneable, Serializable { private static int id = 0; private String name; private String dept; private String summary; private String plain; private String suggestion; private Date date;
public WeekReport() { ++id; }
public WeekReport(String name, String dept, String summary, String plain, String suggestion, Date date) { ++id; this.name = name; this.dept = dept; this.summary = summary; this.plain = plain; this.suggestion = suggestion; this.date = date; }
@Override public Object clone() throws CloneNotSupportedException { try { OutputStream os = new FileOutputStream("a.txt"); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(this); oos.close();
InputStream fis = new FileInputStream("a.txt"); ObjectInputStream ois = new ObjectInputStream(fis); Object clone = ois.readObject(); ois.close(); return clone;
} catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return null; }
====结果输出==== WeekReport{name='Chemlez', dept='IT部门', summary='看了一本书', plain='再看一本书', suggestion='Nothing', date=Wed May 19 13:33:51 CST 2021} WeekReport{name='Chemlez', dept='IT部门', summary='又对了一本书', plain='再读两本书', suggestion='Nothing', date=Wed May 26 13:33:51 CST 2021}
|
从结果看出,我们达到了所要的要求。
但是从代码中,我们又看到了一种问题,就是我们将序列化的路径给写死了,这里又产生了耦合。又有读着肯定想到,那我们干脆使用当前路径下的相对路径不就好了?但是,你有没有想过在Linux
和Windows
的路径设置有很大的区别,其中Linux
中就不存在盘符的级别。如果设定写死的路径,那么就根本没有达到跨平台的目的。那么,有什么好的方法呢?
答案是肯定有的。我们可以将上面序列化到硬盘,再从硬盘反序列化到内存的操作,全部放到内存层面上进行操作。即:将对象序列化到内存上,再从内存上进行反序列化操作。
实现方式二
这里就只给出clone
方法,其他方法都一样就不再重复写出了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Override public Object clone() throws CloneNotSupportedException { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); oos.close(); byte[] bytes = bos.toByteArray(); ByteArrayInputStream bis = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bis); Object clone = ois.readObject(); ois.close(); return clone;
} catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; }
|
至此,代码已经完美解决场景提出的问题。
三、总结
通过原型模式,创建对象新的方式,即用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。创建出的对象,和原对象属性值相同,但是修改不会影响源对象。
又由于原型模式是通过clone
的方式,所以它创建对象的方式会很快。所以当直接创建对象的代价比较大时,则采用这种模式。