1 什么是原型模式?
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式,简称原型模式。
实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。
但是,如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。
举个栗子🌰
我们在这个例子中首先创建了一个文档对象,即 WordDocument ,这个文档中含有文字和图片。
用户经过了长时间的内容编辑后,打算对该文档做进一步的编辑,但是,这个编辑后的文档是否会被采用还不确定,因此,为了安全起见,用户需要将当前文档拷贝一份,然后再在文档副本上进行修改,这与 《Effective Java》 一书中提到的保护性拷贝有些类似,如此,这个原始文档就是样板实例,也就是将要被 “克隆” 的对象,称为原型:
1 | public class WordDocument implements Cloneable { |
通过 WordDocumen 类模拟了 Word 文档中的基本元素,即文字和图片 。 WordDocument 中的 clone 方法用以实现对象克隆。注意,这个方法并不是 Cloneable 接口中的,而是 Object 中的方法。 Cloneable 也是一个标识接口,它表明这个类的对象是可拷贝的。如果没有实现 Cloneable 接口却调用了 clone()函数将抛出异常。在这个示例中,通过实现 Cloneable 接口和覆写 clone 方法实现原型模式。
Client 端的使用:
1 | public class Client { |
输出
1 | ----------Word Content Start------------ |
从输出中可以看到,doc2 是通过 originDoc.clone() 创建的,并且 doc2 第一次输出的时候和 originDoc 输出是一样的,即 doc2 是 originDoc 的一份拷贝,它们的内容是一样的,而 doc2 修改了文本内容以后并不会影响 originDoc 的文本内容,这就保证了 originDoc 的安全性。还需要注意的是,通过 clone 拷贝对象时并不会执行构造函数!因此,如果在构造函数中需要一些特殊的初始化操作的类型,在使用 Cloneable 实现拷贝时,需要注意构造函数不会执行的问题。
2 深拷贝浅拷贝
上述原型模式的实现实际上只是一个浅拷贝,也称为影子拷贝,这份拷贝实际上并不是将原始文档的所有字段都重新构造了一份,而是副本文档的字段引用原始文档的字段
仔细查看,发现 doc2 添加照片之后,originDoc 也被修改了。这是怎么回事呢?
这是因为上文中 WordDocument 的 clone 方法中只是简单地进行浅拷贝,引用类型的新对象 doc2 的 mlmages 只是单纯地指向了 this.mlmages 引用,并没有重新构造一个 mlimages 对象,然后将原始文档中的图片添加到新的 mlmages 对象中,这样就导致 doc2 中的 mlmages 与原始文档中的是同一个对象,因此,修改了其中一个文档中的图片,另一个文档也会受影响。那如何解决这个问题呢?答案就是采用深拷贝,即在拷贝对象时,对于引用型的字段也要采用拷贝的形式,而不是单纯引用的形式。clone 方法修改如下:
1 |
|
这样就可以了,为什么 Text 不会被影响?
因为他是一个 String 类型,赋值的时候可能创建一个或者不创建对象,如果赋值的内容在 java String 池里不存在,会在 java String 池里创建一个创建一个 String 对象,然后指向这个内存地址。而不是去修改当前地址,因为 String 是不可修改的。之后再赋同样的值的时候就不会创建新的对象。
3 Android 源码中的原型模式实现
在上述的文档深拷贝示例中,mlmages 对象调用了 clone0 方法,这就是原型模式。我们知道,mlmages 的实际类型为 ArrayList ,相关源码:
1 | public Object clone() { |
在 Android 中,Intent 可能是我们最早接触的几个类型之一,它用于跳转 Activity 、启动服务、发布广播等功能,它是 Android 系统各组件之间的纽带,也是组件之间传递数据的载体,正是Intent的存在才使得 Android各个组件之间的耦合性很低,Android的组件才如此灵活。
1 | Uri uri = Uri.parse("smsto:0800000123"); |
首先构造了一个发送短信的 Intent 对象,并且设置了短信内容,然后通过原始 Intent 的 clone0 方法构建了一个副本 Intent ,再使用这个副本 Intent 进行跳转
1 |
|
可以看到 clone 方法实际上在内部并没有调用 super.clone()方法来实现对象拷贝,而是调用了 new Intent(this) 。使用 clone 和 new 需要根据构造对象的成本来决定,如果对象的构造成本比较高或者构造较为麻烦,那么使用 clone() 函数效率较高,否则可以使用 new 的形式。
Android 源码 Intent 的查找和匹配 parseBaseApplication 方法是解析 application,可以在文章中搜索。
总结
- 优点
原型模式是在内存中二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。 - 缺点
这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的,在实际开发当中应该注意这个潜在的问题。优点就是减少了约束,缺点也是减少了约束,需要在实际应用时考虑。
感谢
《Android 源码设计模式解析与实战》
以及上文中的链接







