原型模式是一个比较简单,但应用频率比较高的设计模式。
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()方法:
@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;
}
}
现在有这样的业务场景:
每到月初的时候银行会给信用卡用户以邮件的方式发送一份电子账单,包含用户的消费情况、积分等,当然了,还有比较讨厌的广告信。出于个性化服务和投递成功率的考虑,广告信也作为电子账单系统的一个子功能。
这个功能大概这么实现:指定一个模板,从数据库中把客户的信息一个一个地取出,放到模板中生成一份完整的邮件, 然后由发送机进行发送处理。
结合上面的实现思路,相应的类图如下:
图13-2:发送电子账单类图
AdvTemplate是广告信的模板,一般都是从数据库取出,生成一个BO或者是 DTO,这里使用一个静态的值来作代表;Mail是邮件类,发送机发送的就是这个类。
/**
* @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;
}
}
/**
* @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方法
}
/**
* @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的收件人地址和称谓修改了。
解决的办法有很多,其中一种就是通过原型模式。
类图稍作修改:
图13-3:发送电子账单类图
/**
* @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方法
}
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等简单数据类型,引用类型包括类、接口、数组等复杂类 型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的 成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆 对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本 身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
来看一个实例:
/**
* @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的指向地址就不一样了。
还可以通过序列化的方式实现,序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存 中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。
在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的 方法创建一个对象,然后由工厂方法提供给调用者。
参考: 【1】:《设计模式之禅》 【2】:《design-pattern-java》 【3】:《大话设计模式》 【4】:《设计模式之禅》