什么是原型模式?
原型模式是一个比较简单,但应用频率比较高的设计模式。
| Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.(用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对 象。) |
原型模式的通用类图如下:
图13-1:原型模式通用类图

原型模式的核心是一个clone方法,通过该方法进行对象的拷贝,Java 提供了一个Cloneable接口来标示这个对象是可拷贝的,Cloneable接口的作用是标记,在JVM中具有这个标记的对象才有可能被拷贝。那怎么才能从“有可能被拷贝”转换为“可以被拷贝”呢?方法是覆盖 clone()方法:
<span style="color:#141418"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#4078f2">@Override</span> <span style="color:#a626a4">public</span> Mail <span style="color:#4078f2">clone</span>(){}
</code></span></span>
在clone()方法上增加了一个注解@Override——因为覆写了Object类的clone方法。
在Java中原型模式非常简单,通用源码如下:
<span style="color:#141418"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#a626a4">public</span> <span style="color:#a626a4">class</span> <span style="color:#4078f2">PrototypeClass</span> <span style="color:#a626a4">implements</span> <span style="color:#4078f2">Cloneable</span>{
<span style="color:#a0a1a7"><em>//覆写父类Object方法 </em></span>
<span style="color:#4078f2">@Override</span> <span style="color:#a626a4">public</span> PrototypeClass <span style="color:#4078f2">clone</span>(){
<span style="color:#986801">PrototypeClass</span> <span style="color:#986801">prototypeClass</span> <span style="color:#ab5656">=</span> <span style="color:#0184bb">null</span>;
<span style="color:#a626a4">try</span> {
prototypeClass = (PrototypeClass)<span style="color:#c18401">super</span>.clone();
} <span style="color:#a626a4">catch</span> (CloneNotSupportedException e) {
<span style="color:#a0a1a7"><em>//异常处理 </em></span>
}
<span style="color:#a626a4">return</span> prototypeClass;
}
}
</code></span></span>
个性化电子账单
现在有这样的业务场景:
每到月初的时候银行会给信用卡用户以邮件的方式发送一份电子账单,包含用户的消费情况、积分等,当然了,还有比较讨厌的广告信。出于个性化服务和投递成功率的考虑,广告信也作为电子账单系统的一个子功能。
这个功能大概这么实现:指定一个模板,从数据库中把客户的信息一个一个地取出,放到模板中生成一份完整的邮件, 然后由发送机进行发送处理。
使用原型模式前
结合上面的实现思路,相应的类图如下:
图13-2:发送电子账单类图

AdvTemplate是广告信的模板,一般都是从数据库取出,生成一个BO或者是 DTO,这里使用一个静态的值来作代表;Mail是邮件类,发送机发送的就是这个类。
- AdvTemplate类:
<span style="color:#141418"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#a0a1a7"><em>/**
* <span style="color:#a626a4">@author</span> 三分恶
* <span style="color:#a626a4">@date</span> 2020年5月14日
* <span style="color:#a626a4">@description</span> 广告模板类
*/</em></span>
<span style="color:#a626a4">public</span> <span style="color:#a626a4">class</span> <span style="color:#4078f2">AdvTemplate</span> {
<span style="color:#a0a1a7"><em>//广告信名称</em></span>
<span style="color:#a626a4">private</span> <span style="color:#986801">String</span> <span style="color:#986801">advSubject</span> <span style="color:#ab5656">=</span><span style="color:#50a14f">"XX银行国庆信用卡抽奖活动"</span>;
<span style="color:#a0a1a7"><em>//广告信内容</em></span>
<span style="color:#a626a4">private</span> <span style="color:#986801">String</span> <span style="color:#986801">advContext</span> <span style="color:#ab5656">=</span> <span style="color:#50a14f">"国庆抽奖活动通知:只要刷卡就送你一百万!..."</span>;
<span style="color:#a626a4">public</span> String <span style="color:#4078f2">getAdvSubject</span>() {
<span style="color:#a626a4">return</span> advSubject;
}
<span style="color:#a626a4">public</span> String <span style="color:#4078f2">getAdvContext</span>() {
<span style="color:#a626a4">return</span> advContext;
}
}
</code></span></span>
- Mail类:
<span style="color:#141418"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#a0a1a7"><em>/**
* <span style="color:#a626a4">@author</span> 三分恶
* <span style="color:#a626a4">@date</span> 2020年5月14日
* <span style="color:#a626a4">@description</span> 邮件类
*/</em></span>
<span style="color:#a626a4">public</span> <span style="color:#a626a4">class</span> <span style="color:#4078f2">Mail</span> {
<span style="color:#a0a1a7"><em>//收件人</em></span>
<span style="color:#a626a4">private</span> String receiver;
<span style="color:#a0a1a7"><em>//邮件名称 </em></span>
<span style="color:#a626a4">private</span> String subject;
<span style="color:#a0a1a7"><em>//称谓</em></span>
<span style="color:#a626a4">private</span> String appellation;
<span style="color:#a0a1a7"><em>//邮件内容 </em></span>
<span style="color:#a626a4">private</span> String contxt;
<span style="color:#a0a1a7"><em>//邮件的尾部,一般都是加上"XXX版权所有"等信息 </em></span>
<span style="color:#a626a4">private</span> String tail;
<span style="color:#a0a1a7"><em>//构造方法</em></span>
<span style="color:#a626a4">public</span> <span style="color:#4078f2">Mail</span>(AdvTemplate advTemplate) {
<span style="color:#c18401">this</span>.contxt = advTemplate.getAdvContext();
<span style="color:#c18401">this</span>.subject = advTemplate.getAdvSubject();
}
<span style="color:#a0a1a7"><em>//省略getter、setter方法</em></span>
}
</code></span></span>
- Client场景类:
<span style="color:#141418"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#a0a1a7"><em>/**
* <span style="color:#a626a4">@author</span> 三分恶
* <span style="color:#a626a4">@date</span> 2020年5月14日
* <span style="color:#a626a4">@description</span> 场景类
*/</em></span>
<span style="color:#a626a4">public</span> <span style="color:#a626a4">class</span> <span style="color:#4078f2">Client</span> {
<span style="color:#a0a1a7"><em>// 发送账单的数量</em></span>
<span style="color:#a626a4">private</span> <span style="color:#a626a4">static</span> <span style="color:#986801">int</span> <span style="color:#986801">MAX_COUNT</span> <span style="color:#ab5656">=</span> <span style="color:#986801">6</span>;
<span style="color:#a626a4">public</span> <span style="color:#a626a4">static</span> <span style="color:#a626a4">void</span> <span style="color:#4078f2">main</span>(String[] args) {
<span style="color:#a0a1a7"><em>// 模拟发送邮件</em></span>
<span style="color:#986801">int</span> <span style="color:#986801">i</span> <span style="color:#ab5656">=</span> <span style="color:#986801">0</span>;
<span style="color:#a0a1a7"><em>// 把模板定义出来</em></span>
<span style="color:#986801">Mail</span> <span style="color:#986801">mail</span> <span style="color:#ab5656">=</span> <span style="color:#a626a4">new</span> <span style="color:#4078f2">Mail</span>(<span style="color:#a626a4">new</span> <span style="color:#4078f2">AdvTemplate</span>());
mail.setTail(<span style="color:#50a14f">"XX银行版权所有"</span>);
<span style="color:#a626a4">while</span> (i < MAX_COUNT) {
<span style="color:#a0a1a7"><em>// 以下是每封邮件不同的地方</em></span>
mail.setAppellation(getRandString(<span style="color:#986801">5</span>) + <span style="color:#50a14f">" 先生(女士)"</span>);
mail.setReceiver(getRandString(<span style="color:#986801">5</span>) + <span style="color:#50a14f">"@"</span> + getRandString(<span style="color:#986801">8</span>) + <span style="color:#50a14f">".com"</span>);
<span style="color:#a0a1a7"><em>// 然后发送邮件</em></span>
sendMail(mail);
i++;
}
}
<span style="color:#a0a1a7"><em>// 发送邮件</em></span>
<span style="color:#a626a4">public</span> <span style="color:#a626a4">static</span> <span style="color:#a626a4">void</span> <span style="color:#4078f2">sendMail</span>(Mail mail) {
System.out.println(<span style="color:#50a14f">"标题:"</span> + mail.getSubject() + <span style="color:#50a14f">"\t收件人: "</span> + mail.getReceiver() + <span style="color:#50a14f">"\t...发送成功!"</span>);
}
<span style="color:#a0a1a7"><em>// 获得指定长度的随机字符串</em></span>
<span style="color:#a626a4">public</span> <span style="color:#a626a4">static</span> String <span style="color:#4078f2">getRandString</span>(<span style="color:#986801">int</span> maxLength) {
<span style="color:#986801">String</span> <span style="color:#986801">source</span> <span style="color:#ab5656">=</span> <span style="color:#50a14f">"abcdefghijklmnopqrskuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"</span>;
<span style="color:#986801">StringBuffer</span> <span style="color:#986801">sb</span> <span style="color:#ab5656">=</span> <span style="color:#a626a4">new</span> <span style="color:#4078f2">StringBuffer</span>();
<span style="color:#986801">Random</span> <span style="color:#986801">rand</span> <span style="color:#ab5656">=</span> <span style="color:#a626a4">new</span> <span style="color:#4078f2">Random</span>();
<span style="color:#a626a4">for</span> (<span style="color:#986801">int</span> <span style="color:#986801">i</span> <span style="color:#ab5656">=</span> <span style="color:#986801">0</span>; i < maxLength; i++) {
sb.append(source.charAt(rand.nextInt(source.length())));
}
<span style="color:#a626a4">return</span> sb.toString();
}
}
</code></span></span>
运行结果:

OK,发送广告信的功能到此实现了。
但是存在一个问题,这个程序时单线程的,按照一封邮件发出去需要0.02秒,600万封邮件需要33个小时,也就是一个整天都发送不完,今天的没发送完,明天的账单又产生了。
如果用多线程的方式呢?那么线程安全的问题又来了。产生第一封邮件对象,放到线程1中运行,还没有发送出去;线程2也启动了,直接就把邮件对 象mail的收件人地址和称谓修改了。
解决的办法有很多,其中一种就是通过原型模式。
使用原型模式后
类图稍作修改:
图13-3:发送电子账单类图

- Mail类实现Cloneable接口,覆写clone()方法:
<span style="color:#141418"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#a0a1a7"><em>/**
* <span style="color:#a626a4">@author</span> 三分恶
* <span style="color:#a626a4">@date</span> 2020年5月14日
* <span style="color:#a626a4">@description</span> 邮件类
*/</em></span>
<span style="color:#a626a4">public</span> <span style="color:#a626a4">class</span> <span style="color:#4078f2">Mail</span> <span style="color:#a626a4">implements</span> <span style="color:#4078f2">Cloneable</span>{
<span style="color:#a0a1a7"><em>//收件人</em></span>
<span style="color:#a626a4">private</span> String receiver;
<span style="color:#a0a1a7"><em>//邮件名称 </em></span>
<span style="color:#a626a4">private</span> String subject;
<span style="color:#a0a1a7"><em>//称谓</em></span>
<span style="color:#a626a4">private</span> String appellation;
<span style="color:#a0a1a7"><em>//邮件内容 </em></span>
<span style="color:#a626a4">private</span> String contxt;
<span style="color:#a0a1a7"><em>//邮件的尾部,一般都是加上"XXX版权所有"等信息 </em></span>
<span style="color:#a626a4">private</span> String tail;
<span style="color:#a0a1a7"><em>//构造方法</em></span>
<span style="color:#a626a4">public</span> <span style="color:#4078f2">Mail</span>(AdvTemplate advTemplate) {
<span style="color:#c18401">this</span>.contxt = advTemplate.getAdvContext();
<span style="color:#c18401">this</span>.subject = advTemplate.getAdvSubject();
}
<span style="color:#a0a1a7"><em>/**
* 覆写clone方法
*/</em></span>
<span style="color:#4078f2">@Override</span>
<span style="color:#a626a4">public</span> Mail <span style="color:#4078f2">clone</span>(){
Mail mail=<span style="color:#0184bb">null</span>;
<span style="color:#a626a4">try</span> {
mail=(Mail) <span style="color:#c18401">super</span>.clone();
} <span style="color:#a626a4">catch</span> (CloneNotSupportedException e) {
<span style="color:#a0a1a7"><em>// TODO Auto-generated catch block</em></span>
e.printStackTrace();
}
<span style="color:#a626a4">return</span> mail;
}
<span style="color:#a0a1a7"><em>//省略getter、setter方法</em></span>
}
</code></span></span>
- Client场景类的修改:
<span style="color:#141418"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#a626a4">public</span> <span style="color:#a626a4">static</span> <span style="color:#a626a4">void</span> <span style="color:#4078f2">main</span>(String[] args) {
<span style="color:#a0a1a7"><em>// 模拟发送邮件</em></span>
<span style="color:#986801">int</span> <span style="color:#986801">i</span> <span style="color:#ab5656">=</span> <span style="color:#986801">0</span>;
<span style="color:#a0a1a7"><em>// 把模板定义出来</em></span>
<span style="color:#986801">Mail</span> <span style="color:#986801">mail</span> <span style="color:#ab5656">=</span> <span style="color:#a626a4">new</span> <span style="color:#4078f2">Mail</span>(<span style="color:#a626a4">new</span> <span style="color:#4078f2">AdvTemplate</span>());
mail.setTail(<span style="color:#50a14f">"XX银行版权所有"</span>);
<span style="color:#a626a4">while</span> (i < MAX_COUNT) {
<span style="color:#a0a1a7"><em>// 以下是每封邮件不同的地方</em></span>
<span style="color:#a0a1a7"><em>//这里使用clone方法clone对象</em></span>
<span style="color:#986801">Mail</span> <span style="color:#986801">cloneMail</span> <span style="color:#ab5656">=</span> mail.clone();
<span style="color:#a0a1a7"><em>//使用clone的对象</em></span>
cloneMail.setAppellation(getRandString(<span style="color:#986801">5</span>) + <span style="color:#50a14f">" 先生(女士)"</span>);
cloneMail.setReceiver(getRandString(<span style="color:#986801">5</span>) + <span style="color:#50a14f">"@"</span> + getRandString(<span style="color:#986801">8</span>) + <span style="color:#50a14f">".com"</span>);
<span style="color:#a0a1a7"><em>// 然后发送邮件</em></span>
sendMail(mail);
i++;
}
}
</code></span></span>
在设置邮件不同属性的地方通过clone的方式产生一个新的对象,然后再修改细节的数据,如设置称谓、设置收件人地址,这样即使是多线程也不受影响。
原型模式的优缺点
原型模式的优点
原型模式的主要优点如下:
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
- 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的 工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
- 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起 来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
原型模式的缺点
原型模式的主要缺点如下:
- 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进 行改造时,需要修改源代码,违背了“开闭原则”。
- 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了 实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
深克隆与浅克隆
在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括 int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类 型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。
浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的 成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆 对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本 身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。

来看一个实例:
- 在Thing类中定义一个私有变量arrayLis,类型为ArrayList,然后通过setValue和getValue 分别进行设置和取值
<span style="color:#141418"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#a0a1a7"><em>/**
* <span style="color:#a626a4">@author</span> 三分恶
* <span style="color:#a626a4">@date</span> 2020年5月14日
* <span style="color:#a626a4">@description</span>
*/</em></span>
<span style="color:#a626a4">public</span> <span style="color:#a626a4">class</span> <span style="color:#4078f2">Thing</span> <span style="color:#a626a4">implements</span> <span style="color:#4078f2">Cloneable</span>{
<span style="color:#a0a1a7"><em>//定义一个私有变量 </em></span>
<span style="color:#a626a4">private</span> ArrayList<String> arrayList = <span style="color:#a626a4">new</span> <span style="color:#4078f2">ArrayList</span><String>();
<span style="color:#4078f2">@Override</span>
<span style="color:#a626a4">public</span> Thing <span style="color:#4078f2">clone</span>() {
<span style="color:#986801">Thing</span> <span style="color:#986801">thing</span> <span style="color:#ab5656">=</span> <span style="color:#0184bb">null</span>;
<span style="color:#a626a4">try</span> {
thing = (Thing) <span style="color:#c18401">super</span>.clone();
} <span style="color:#a626a4">catch</span> (CloneNotSupportedException e) {
e.printStackTrace();
}
<span style="color:#a626a4">return</span> thing;
}
<span style="color:#a0a1a7"><em>//设置HashMap的值 </em></span>
<span style="color:#a626a4">public</span> <span style="color:#a626a4">void</span> <span style="color:#4078f2">setValue</span>(String value){
<span style="color:#c18401">this</span>.arrayList.add(value); }
<span style="color:#a0a1a7"><em>// 取得arrayList的值</em></span>
<span style="color:#a626a4">public</span> ArrayList<String> <span style="color:#4078f2">getValue</span>() {
<span style="color:#a626a4">return</span> <span style="color:#c18401">this</span>.arrayList;
}
}
</code></span></span>
- 在场景类中克隆
<span style="color:#141418"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#a0a1a7"><em>/**
* <span style="color:#a626a4">@author</span> 三分恶
* <span style="color:#a626a4">@date</span> 2020年5月14日
* <span style="color:#a626a4">@description</span> 浅克隆测试
*/</em></span>
<span style="color:#a626a4">public</span> <span style="color:#a626a4">class</span> <span style="color:#4078f2">ShallowCloneClient</span> {
<span style="color:#a626a4">public</span> <span style="color:#a626a4">static</span> <span style="color:#a626a4">void</span> <span style="color:#4078f2">main</span>(String[] args) {
<span style="color:#a0a1a7"><em>// 产生一个对象</em></span>
<span style="color:#986801">Thing</span> <span style="color:#986801">thing</span> <span style="color:#ab5656">=</span> <span style="color:#a626a4">new</span> <span style="color:#4078f2">Thing</span>();
<span style="color:#a0a1a7"><em>// 设置一个值</em></span>
thing.setValue(<span style="color:#50a14f">"二锤子"</span>);
<span style="color:#a0a1a7"><em>// 拷贝一个对象</em></span>
<span style="color:#986801">Thing</span> <span style="color:#986801">cloneThing</span> <span style="color:#ab5656">=</span> thing.clone();
cloneThing.setValue(<span style="color:#50a14f">"三棒子"</span>);
System.out.println(thing.getValue());
}
}
</code></span></span>
在这个例子中,对象thing和cloneThing的arrayList都指向了同一个地址,所以运行结果:

这里就存在风险,两个对象共享了一个私有变量,都能对这个变量进行修改。
使用原型模式时,引用的成员变量必须满足两个条件才不会被克隆:一是类的成 员变量,而不是方法内变量;二是必须是一个可变的引用对象,而不是一个原始类型或不可 变对象。
深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象, 深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

将Thing类的clone方法进行修改:
<span style="color:#141418"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#4078f2">@Override</span>
<span style="color:#a626a4">public</span> Thing <span style="color:#4078f2">clone</span>() {
<span style="color:#986801">Thing</span> <span style="color:#986801">thing</span> <span style="color:#ab5656">=</span> <span style="color:#0184bb">null</span>;
<span style="color:#a626a4">try</span> {
thing = (Thing) <span style="color:#c18401">super</span>.clone();
<span style="color:#a0a1a7"><em>//对私有的类变量进行独立的拷贝</em></span>
thing.arrayList = (ArrayList<String>)<span style="color:#c18401">this</span>.arrayList.clone();
} <span style="color:#a626a4">catch</span> (CloneNotSupportedException e) {
e.printStackTrace();
}
<span style="color:#a626a4">return</span> thing;
}
</code></span></span>
改动很少,对私有的类变量进行独立的拷贝,这样一来,两个对象的arrayList的指向地址就不一样了。

还可以通过序列化的方式实现,序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存 中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。
原型模式的应用场景
-
资源优化场景
类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。 -
性能和安全要求的场景
通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。 -
一个对象多个修改者的场景 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑 使用原型模式拷贝多个对象供调用者使用。
在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的 方法创建一个对象,然后由工厂方法提供给调用者。
原型模式:原理、优缺点及应用场景


被折叠的 条评论
为什么被折叠?



