设计模式—— 十三 :原型模式
@
什么是原型模式?
原型模式是一个比较简单,但应用频率比较高的设计模式。
Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.(用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对 象。) |
原型模式的通用类图如下:
原型模式的核心是一个clone方法,通过该方法进行对象的拷贝,Java 提供了一个Cloneable接口来标示这个对象是可拷贝的,Cloneable接口的作用是标记,在JVM中具有这个标记的对象才有可能被拷贝。那怎么才能从“有可能被拷贝”转换为“可以被拷贝”呢?方法是覆盖 clone()方法:
@Override public Mail clone(){}
在clone()方法上增加了一个注解@Override——因为覆写了Object类的clone方法。
在Java中原型模式非常简单,通用源码如下:
public class PrototypeClass implements Cloneable{
//覆写父类Object方法
@Override public PrototypeClass clone(){
PrototypeClass prototypeClass = null;
try {
prototypeClass = (PrototypeClass)super.clone();
} catch (CloneNotSupportedException e) {
//异常处理
}
return prototypeClass;
}
}
个性化电子账单
现在有这样的业务场景:
每到月初的时候银行会给信用卡用户以邮件的方式发送一份电子账单,包含用户的消费情况、积分等,当然了,还有比较讨厌的广告信。出于个性化服务和投递成功率的考虑,广告信也作为电子账单系统的一个子功能。
这个功能大概这么实现:指定一个模板,从数据库中把客户的信息一个一个地取出,放到模板中生成一份完整的邮件, 然后由发送机进行发送处理。
使用原型模式前
结合上面的实现思路,相应的类图如下:
AdvTemplate是广告信的模板,一般都是从数据库取出,生成一个BO或者是 DTO,这里使用一个静态的值来作代表;Mail是邮件类,发送机发送的就是这个类。
- AdvTemplate类:
/**
* @author 三分恶
* @date 2020年5月14日
* @description 广告模板类
*/
public class AdvTemplate {
//广告信名称
private String advSubject ="XX银行国庆信用卡抽奖活动";
//广告信内容
private String advContext = "国庆抽奖活动通知:只要刷卡就送你一百万!...";
public String getAdvSubject() {
return advSubject;
}
public String getAdvContext() {
return advContext;
}
}
- Mail类:
/**
* @author 三分恶
* @date 2020年5月14日
* @description 邮件类
*/
public class Mail {
//收件人
private String receiver;
//邮件名称
private String subject;
//称谓
private String appellation;
//邮件内容
private String contxt;
//邮件的尾部,一般都是加上"XXX版权所有"等信息
private String tail;
//构造方法
public Mail(AdvTemplate advTemplate) {
this.contxt = advTemplate.getAdvContext();
this.subject = advTemplate.getAdvSubject();
}
//省略getter、setter方法
}
- Client场景类:
/**
* @author 三分恶
* @date 2020年5月14日
* @description 场景类
*/
public class Client {
// 发送账单的数量
private static int MAX_COUNT = 6;
public static void main(String[] args) {
// 模拟发送邮件
int i = 0;
// 把模板定义出来
Mail mail = new Mail(new AdvTemplate());
mail.setTail("XX银行版权所有");
while (i < MAX_COUNT) {
// 以下是每封邮件不同的地方
mail.setAppellation(getRandString(5) + " 先生(女士)");
mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
// 然后发送邮件
sendMail(mail);
i++;
}
}
// 发送邮件
public static void sendMail(Mail mail) {
System.out.println("标题:" + mail.getSubject() + "\t收件人: " + mail.getReceiver() + "\t...发送成功!");
}
// 获得指定长度的随机字符串
public static String getRandString(int maxLength) {
String source = "abcdefghijklmnopqrskuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuffer sb = new StringBuffer();
Random rand = new Random();
for (int i = 0; i < maxLength; i++) {
sb.append(source.charAt(rand.nextInt(source.length())));
}
return sb.toString();
}
}
运行结果:
OK,发送广告信的功能到此实现了。
但是存在一个问题,这个程序时单线程的,按照一封邮件发出去需要0.02秒,600万封邮件需要33个小时,也就是一个整天都发送不完,今天的没发送完,明天的账单又产生了。
如果用多线程的方式呢?那么线程安全的问题又来了。产生第一封邮件对象,放到线程1中运行,还没有发送出去;线程2也启动了,直接就把邮件对 象mail的收件人地址和称谓修改了。
解决的办法有很多,其中一种就是通过原型模式。
使用原型模式后
类图稍作修改:
- Mail类实现Cloneable接口,覆写clone()方法:
/**
* @author 三分恶
* @date 2020年5月14日
* @description 邮件类
*/
public class Mail implements Cloneable{
//收件人
private String receiver;
//邮件名称
private String subject;
//称谓
private String appellation;
//邮件内容
private String contxt;
//邮件的尾部,一般都是加上"XXX版权所有"等信息
private String tail;
//构造方法
public Mail(AdvTemplate advTemplate) {
this.contxt = advTemplate.getAdvContext();
this.subject = advTemplate.getAdvSubject();
}
/**
* 覆写clone方法
*/
@Override
public Mail clone(){
Mail mail=null;
try {
mail=(Mail) super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return mail;
}
//省略getter、setter方法
}
- Client场景类的修改:
public static void main(String[] args) {
// 模拟发送邮件
int i = 0;
// 把模板定义出来
Mail mail = new Mail(new AdvTemplate());
mail.setTail("XX银行版权所有");
while (i < MAX_COUNT) {
// 以下是每封邮件不同的地方
//这里使用clone方法clone对象
Mail cloneMail = mail.clone();
//使用clone的对象
cloneMail.setAppellation(getRandString(5) + " 先生(女士)");
cloneMail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
// 然后发送邮件
sendMail(mail);
i++;
}
}
在设置邮件不同属性的地方通过clone的方式产生一个新的对象,然后再修改细节的数据,如设置称谓、设置收件人地址,这样即使是多线程也不受影响。
原型模式的优缺点
原型模式的优点
原型模式的主要优点如下:
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
- 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的 工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
- 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起 来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
原型模式的缺点
原型模式的主要缺点如下:
- 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进 行改造时,需要修改源代码,违背了“开闭原则”。
- 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了 实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
深克隆与浅克隆
在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括 int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类 型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。
浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的 成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆 对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本 身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
来看一个实例:
- 在Thing类中定义一个私有变量arrayLis,类型为ArrayList,然后通过setValue和getValue 分别进行设置和取值
/**
* @author 三分恶
* @date 2020年5月14日
* @description
*/
public class Thing implements Cloneable{
//定义一个私有变量
private ArrayList<String> arrayList = new ArrayList<String>();
@Override
public Thing clone() {
Thing thing = null;
try {
thing = (Thing) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return thing;
}
//设置HashMap的值
public void setValue(String value){
this.arrayList.add(value); }
// 取得arrayList的值
public ArrayList<String> getValue() {
return this.arrayList;
}
}
- 在场景类中克隆
/**
* @author 三分恶
* @date 2020年5月14日
* @description 浅克隆测试
*/
public class ShallowCloneClient {
public static void main(String[] args) {
// 产生一个对象
Thing thing = new Thing();
// 设置一个值
thing.setValue("二锤子");
// 拷贝一个对象
Thing cloneThing = thing.clone();
cloneThing.setValue("三棒子");
System.out.println(thing.getValue());
}
}
在这个例子中,对象thing和cloneThing的arrayList都指向了同一个地址,所以运行结果:
这里就存在风险,两个对象共享了一个私有变量,都能对这个变量进行修改。
使用原型模式时,引用的成员变量必须满足两个条件才不会被克隆:一是类的成 员变量,而不是方法内变量;二是必须是一个可变的引用对象,而不是一个原始类型或不可 变对象。
深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象, 深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
将Thing类的clone方法进行修改:
@Override
public Thing clone() {
Thing thing = null;
try {
thing = (Thing) super.clone();
//对私有的类变量进行独立的拷贝
thing.arrayList = (ArrayList<String>)this.arrayList.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return thing;
}
改动很少,对私有的类变量进行独立的拷贝,这样一来,两个对象的arrayList的指向地址就不一样了。
还可以通过序列化的方式实现,序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存 中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。
原型模式的应用场景
-
资源优化场景
类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。 -
性能和安全要求的场景
通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。 -
一个对象多个修改者的场景 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑 使用原型模式拷贝多个对象供调用者使用。
在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的 方法创建一个对象,然后由工厂方法提供给调用者。
参考:
【1】:《设计模式之禅》
【2】:《design-pattern-java》
【3】:《大话设计模式》
【4】:《设计模式之禅》