1 什么是原型模式?

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式,简称原型模式。

实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。

但是,如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。

举个栗子🌰

我们在这个例子中首先创建了一个文档对象,即 WordDocument ,这个文档中含有文字和图片。
用户经过了长时间的内容编辑后,打算对该文档做进一步的编辑,但是,这个编辑后的文档是否会被采用还不确定,因此,为了安全起见,用户需要将当前文档拷贝一份,然后再在文档副本上进行修改,这与 《Effective Java》 一书中提到的保护性拷贝有些类似,如此,这个原始文档就是样板实例,也就是将要被 “克隆” 的对象,称为原型:

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
public class WordDocument implements Cloneable {
private String mText;
private ArrayList<String> mImages = new ArrayList<>();

@Override
public WordDocument clone() {
try {
WordDocument doc = (WordDocument) super.clone();
doc.mText = this.mText;
doc.mImages = this.mImages;
return doc;
} catch (Exception e) {

}
return null;
}

public String getmText() {
return mText;
}

public void setmText(String mText) {
this.mText = mText;
}

public ArrayList<String> getmImages() {
return mImages;
}

public void addImages(String mImages) {
this.mImages.add(mImages);
}

public void showDocument() {
System.out.println("----------Word Content Start------------");
System.out.println("Text :" + mText);
System.out.println("Image List :");
for (String mImage : mImages) {
System.out.println("image name :" + mImage);
}
System.out.println("----------Word Content End------------");

}
}

通过 WordDocumen 类模拟了 Word 文档中的基本元素,即文字和图片 。 WordDocument 中的 clone 方法用以实现对象克隆。注意,这个方法并不是 Cloneable 接口中的,而是 Object 中的方法。 Cloneable 也是一个标识接口,它表明这个类的对象是可拷贝的。如果没有实现 Cloneable 接口却调用了 clone()函数将抛出异常。在这个示例中,通过实现 Cloneable 接口和覆写 clone 方法实现原型模式。
Client 端的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Client {
public void main() {
WordDocument originDoc = new WordDocument();
originDoc.setmText("这是一篇文档");
originDoc.addImages("图片 1");
originDoc.addImages("图片 2");
originDoc.addImages("图片 3");
originDoc.showDocument();

WordDocument doc2 = originDoc.clone();
doc2.showDocument();

doc2.setmText("修改");
doc2.addImages("哈哈 123");
doc2.showDocument();

originDoc.showDocument();
}
}

输出

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
----------Word Content Start------------
Text :这是一篇文档
Image List :
image name :图片 1
image name :图片 2
image name :图片 3
----------Word Content End------------
----------Word Content Start------------
Text :这是一篇文档
Image List :
image name :图片 1
image name :图片 2
image name :图片 3
----------Word Content End------------
----------Word Content Start------------
Text :修改
Image List :
image name :图片 1
image name :图片 2
image name :图片 3
image name :哈哈 123
----------Word Content End------------
----------Word Content Start------------
Text :这是一篇文档
Image List :
image name :图片 1
image name :图片 2
image name :图片 3
image name :哈哈 123
----------Word Content End------------

从输出中可以看到,doc2 是通过 originDoc.clone() 创建的,并且 doc2 第一次输出的时候和 originDoc 输出是一样的,即 doc2originDoc 的一份拷贝,它们的内容是一样的,而 doc2 修改了文本内容以后并不会影响 originDoc 的文本内容,这就保证了 originDoc 的安全性。还需要注意的是,通过 clone 拷贝对象时并不会执行构造函数!因此,如果在构造函数中需要一些特殊的初始化操作的类型,在使用 Cloneable 实现拷贝时,需要注意构造函数不会执行的问题。

2 深拷贝浅拷贝

上述原型模式的实现实际上只是一个浅拷贝,也称为影子拷贝,这份拷贝实际上并不是将原始文档的所有字段都重新构造了一份,而是副本文档的字段引用原始文档的字段

仔细查看,发现 doc2 添加照片之后,originDoc 也被修改了。这是怎么回事呢?
这是因为上文中 WordDocumentclone 方法中只是简单地进行浅拷贝,引用类型的新对象 doc2mlmages 只是单纯地指向了 this.mlmages 引用,并没有重新构造一个 mlimages 对象,然后将原始文档中的图片添加到新的 mlmages 对象中,这样就导致 doc2 中的 mlmages 与原始文档中的是同一个对象,因此,修改了其中一个文档中的图片,另一个文档也会受影响。那如何解决这个问题呢?答案就是采用深拷贝,即在拷贝对象时,对于引用型的字段也要采用拷贝的形式,而不是单纯引用的形式。clone 方法修改如下:

1
2
3
4
5
6
7
8
9
10
11
@Override
public WordDocument clone() {
try {
WordDocument doc = (WordDocument) super.clone();
doc.mText = this.mText;
doc.mImages = (ArrayList<String>) this.mImages.clone();
return doc;
} catch (Exception e) {
}
return null;
}

这样就可以了,为什么 Text 不会被影响?

因为他是一个 String 类型,赋值的时候可能创建一个或者不创建对象,如果赋值的内容在 java String 池里不存在,会在 java String 池里创建一个创建一个 String 对象,然后指向这个内存地址。而不是去修改当前地址,因为 String 是不可修改的。之后再赋同样的值的时候就不会创建新的对象。

3 Android 源码中的原型模式实现

在上述的文档深拷贝示例中,mlmages 对象调用了 clone0 方法,这就是原型模式。我们知道,mlmages 的实际类型为 ArrayList ,相关源码:

1
2
3
4
5
6
7
8
9
10
11
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}

Android 中,Intent 可能是我们最早接触的几个类型之一,它用于跳转 Activity 、启动服务、发布广播等功能,它是 Android 系统各组件之间的纽带,也是组件之间传递数据的载体,正是Intent的存在才使得 Android各个组件之间的耦合性很低,Android的组件才如此灵活。

1
2
3
4
5
6
Uri uri = Uri.parse("smsto:0800000123");
Intent shareIntent = new Intent(Intent.ACTION_SENDTO,uri);
shareIntent.putExtra("sms_body","The SMS test");

Intent intent= (Intent) shareIntent.clone();
startActivity(intent);

首先构造了一个发送短信的 Intent 对象,并且设置了短信内容,然后通过原始 Intentclone0 方法构建了一个副本 Intent ,再使用这个副本 Intent 进行跳转

1
2
3
4
@Override
public Object clone() {
return new Intent(this);
}

可以看到 clone 方法实际上在内部并没有调用 super.clone()方法来实现对象拷贝,而是调用了 new Intent(this) 。使用 clonenew 需要根据构造对象的成本来决定,如果对象的构造成本比较高或者构造较为麻烦,那么使用 clone() 函数效率较高,否则可以使用 new 的形式。

Android 源码 Intent 的查找和匹配 parseBaseApplication 方法是解析 application,可以在文章中搜索。

总结

  • 优点
    原型模式是在内存中二进制流的拷贝,要比直接 new 一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。
  • 缺点
    这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的,在实际开发当中应该注意这个潜在的问题。优点就是减少了约束,缺点也是减少了约束,需要在实际应用时考虑。

感谢

设计模式之美

《Android 源码设计模式解析与实战》

以及上文中的链接