-
个性电子账单
现在电子账单越来越流行了,比如你的信用卡,没到月初的时候银行就会发一份电子邮件给你,说你这个月消费了多少,什么时候消费的,积分是多少等,这是每个月发一次的。还有就是各大银行发的广告信,虽然电子邮件的模板大致都相同,但是有些地方是有区别的,就是客户的称呼,银行发送该类邮件是有要求的:
- 个性化服
一般银行都要求个性化服务,发过去的邮件上总有一些个人信息,比如"xx先生","xx女士"等。 -
递送成功率
邮件的递送成功率是有一定得要求,由于大批量地发送邮件会被接收方邮件服务器误认是垃圾邮件,因此要在邮件头要增加一些伪造数据,以规避被反垃圾邮件引擎误认为是垃圾邮件。
从这两方面考虑广告信的发送也是电子账单系统(电子账单系统一般包括:账单分析、广告信息管理、发送队列管理、发送机、退信处理、报表管理等)的一个子功能,我们今天就俩考虑一下广告信这个模块是怎么开发的。那既然是广告信,肯定要一个模板,然后在从数据库中把客户的信息一个个的取出来,放到模板中生成一份完整的邮件,然后扔给发送机进行处理,类图13-1:
在类图中AdvTemplate是广告信息的模板,一般都是从数据库中取出,生成一个BO或是DTO,我们这里使用一个静态值做代表;Mail是一封邮件类,发送机发送的就是这个类。我们先来看AdvTemplate和Mail,代码如下:
public class AdvTemplate {
private String advSubject = "xx银行国庆信用卡抽奖活动";
private String advContext = "国庆抽奖活动通知;只要刷卡就送你一百万!...";
public String getAdvSubject(){
return this.advSubject;
}
public String getAdvContext(){
return this.advContext;
}
}
public class Mail {
private String recevier;
private String subject;
private String appellation;
private String contxt;
private String tail;
public Mail(AdvTemplate advTemplate){
this.subject = advTemplate.getAdvSubject();
this.contxt = advTemplate.getAdvContext();
}
public String getRecevier() {
return recevier;
}
public void setRecevier(String recevier) {
this.recevier = recevier;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getAppellation() {
return appellation;
}
public void setAppellation(String appellation) {
this.appellation = appellation;
}
public String getContxt() {
return contxt;
}
public void setContxt(String contxt) {
this.contxt = contxt;
}
public String getTail() {
return tail;
}
public void setTail(String tail) {
this.tail = tail;
}
}
Mail就是一个业务对象,虽然比较长。我们在看看业务场景类是如何对邮件进行处理的,代码如下:
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.setRecevier(getRandString(5)+"@"+getRandString(8)+".com");
sendMail(mail);
i++;
}
}
//发送邮件
public static void sendMail(Mail mail){
System.out.println("标题:"+mail.getSubject()+"\t收件人;"+mail.getRecevier()+"\t...发送成功!");
}
private static String getRandString(int maxLength) {
String source = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM";
StringBuilder sb = new StringBuilder();
Random rand = new Random();
for(int i=0;i<maxLength;i++){
sb.append(source.charAt(rand.nextInt(source.length())));
}
return sb.toString();
}
}
发送邮件一般都是这么做法,我们仔细想想,这个程序是否有问题?这是一个线程在运行,那按照发送一封邮件需要0.02秒(够小了,你还要到数据库中取数据呢),600万封邮件需要33小时,也就是一个整天都发送不完,今天的没发送完,明天的账单又产生了,日积月累,激起甲方人员一堆抱怨,那怎么办?那我们报sendMail修改为多线程,但是只把sendMail修改为多线程还是有问题呀,产生第一封邮件对象,放到线程1中运行,还没有发送出去;线程2也启动了,直接就把邮件对象mail的收件人地址和称谓修改掉了,线程不安全了(一般多线程就不是怎么个写法了,就不会把mail在这地方去new了,但是这里mail是模板,我们且往下面看)。说道这里,你会说这有N种解决办法,其中一种是使用一种新型模式来解决这个问题:通过对象的复制功能来解决这个问题,类图稍作修改,如图13-2所示:
增加了一个Cloneable接口(java自带的一个接口),Mail实现了这个接口,在Mail类中覆写clone()方法,我们来看Mail类的改变,代码如下:
public class Mail implements Cloneable {
private String recevier;
private String subject;
private String appellation;
private String contxt;
private String tail;
public Mail(AdvTemplate advTemplate){
this.subject = advTemplate.getAdvSubject();
this.contxt = advTemplate.getAdvContext();
}
@Override
protected Mail clone(){
Mail mail = null;
try {
mail = (Mail) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return mail;
}
public String getRecevier() {
return recevier;
}
public void setRecevier(String recevier) {
this.recevier = recevier;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getAppellation() {
return appellation;
}
public void setAppellation(String appellation) {
this.appellation = appellation;
}
public String getContxt() {
return contxt;
}
public void setContxt(String contxt) {
this.contxt = contxt;
}
public String getTail() {
return tail;
}
public void setTail(String tail) {
this.tail = tail;
}
}
注意看粗体部分,实现一个接口,并重写了clone方法,大家可能看着这个类有点奇怪,先保留你的好奇,我们继续讲下去,稍后会给你清晰的答案。我们再来看场景Client的变化,代码如下:
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 cloneMail = mail.clone();
cloneMail.setAppellation(getRandString(5)+"先生(女士)");
cloneMail.setRecevier(getRandString(5)+"@"+getRandString(8)+".com");
sendMail(cloneMail);
i++;
}
}
//发送邮件
public static void sendMail(Mail mail){
System.out.println("标题:"+mail.getSubject()+"\t收件人;"+mail.getRecevier()+"\t...发送成功!");
}
private static String getRandString(int maxLength) {
String source = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM";
StringBuilder sb = new StringBuilder();
Random rand = new Random();
for(int i=0;i<maxLength;i++){
sb.append(source.charAt(rand.nextInt(source.length())));
}
return sb.toString();
}
}
一样完成了电子广告的发送,而且sendMail即使是多线程也没有关系(这里没有用多线程写)。注意,看Client中的粗体字mail.clone()这个方法,把对象复制一份,产生一个新的对象,和原有对象一样,然后在修改细节的数据,如设置称谓、收件人地址等,这种不通过new关键字来产生一个对象,而是通过对象复制来实现的模式就叫做原型模式。(这个地方用到了java提供的并发包中两个很重要的思想之一叫做写时复制,这是保证处理高并发时一种线程安全的做法)
-
原型模式的定义
原型模式(Prototype Pattern)的简单程度仅次于单例模式和迭代器模式。正是由于简单,使用的场景才非常的多,其定义如下:
Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.(用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。)
原型模式的通用类图13-3:
原型模式的核心是一个clone方法,通过该方法进行对象的拷贝,java提供了一个cloneable接口来标识这个对象是可拷贝的,这个接口没有方法,只是标记作用,在jvm中具有这个标记的对象才有可能被拷贝。那怎么才能从"有可能被拷贝"转换为"可以被拷贝呢"?方法是覆盖clone()方法,看看我们上面Mail类中的clone方法,在clone()方法中增加了一个注解@Override,这个是覆写的Object的clone方法!我们来看看原型模式的通用源码,代码如下:
public class PrototypeClass implements Cloneable{
@Override
public PrototypeClass clone(){
PrototypeClass prototypeClass = null;
try {
prototypeClass = (PrototypeClass)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return prototypeClass;
}
}
实现一个接口Cloneable,然后重写clone方法,就完成了一个原型模式!(其实在这里有一个疑问,这个clone方法里面为什么使用的super.clone(),我百度了一下没看到很好的解释,我是这样理解的,可以看Object中的clone方法有一个关键词natice,表示这是个本地方法调用的低层代码,然后我们覆写的clone方法实际比没有实现复制对象的逻辑,所以必须使用super.clone来调用Object的clone方法;不知道对不对哈)
-
原型模式的应用
3.1 原型模式的优点
- 性能优良
原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量对象时,原型模式可以更好地体现 其优点。 - 逃避构造函数的约束
这既是它的有点也是缺点,直接在内存中拷贝,构造函数是不会执行的。优点就是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。(这个只能说遇见了才比较清楚是怎么回事,我目前还没见过,实际上我开发中用到的clone的都比较少,clone也是有很多限制的,不小心就很容易出现问题)
3.2 原型模式的使用场景 - 资源优化场景
类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等 - 性能和安全要求的场景
通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。 - 一个对象多个修改者的场景
一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与java融为一体,大家可以随手哪来使用。
-
原型模式的注意事项
原型模式虽然简单,但是在java中使用使用原型模式也就是clone方法还是有一些注意事项的,通过例子学习。
4.1构造函数不会被执行
一个实现了Cloneable并重写了clone方法的类A,有一个无参构造或有参构造B,通过new 关键字产生一个对象S,在然后通过S.clone()方式产生了一个新的对象T,那么在对象拷贝时构造函数B是不会被执行的,通过代码来看:
public class Client {
public static class Thing implements Cloneable{
public Thing(){
System.out.println("构造函数被执行了。。。。");
}
@Override
public Thing clone(){
Thing thing = null;
try{
thing = (Thing)super.clone();
}catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return thing;
}
}
public static void main(String[] args) {
Thing thing = new Thing();
Thing cloneThing = thing.clone();
System.out.println(thing);
System.out.println(cloneThing);
}
}
对象拷贝时构造函数确实没有被执行,这点从原理上也是讲的通的,Object类的clone方法的原理是从内存中(具体说的就是堆内存)以二进制流的方式进行拷贝,重新分配一个内存块,那构造函数没有被执行也是正常的了。
4.2 浅拷贝和深拷贝
在学习浅拷贝和深拷贝之前我们先看个例子,代码如下:
public class Client01 {
public static class Thing implements Cloneable{
private ArrayList<String> arrayList = new ArrayList<>();
@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;
}
}
public static void main(String[] args) {
Thing thing = new Thing();
thing.setValue("zhangsan");
Thing cloneThing = thing.clone();
cloneThing.setValue("lisi");
System.out.println(thing.getValue());
}
}
//运行结果
[zhangsan, lisi]
为什么会有lisi呢?是因为java做了一个偷懒的拷贝动作,Object类提供的方法clone只是拷贝本对象,其对象内部数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址,这种拷贝就叫做浅拷贝。确实是非常浅两个对象共享了一个私有变量,你改我改大家改,是一种非常不安全的方式,在实际项目中使用的还是比较少的(当然,这也是一种"危机"环境的一种救命方式)。为什么在Mail那个类中就可以使用String类型,而不会产生由浅拷贝带来的问题呢?内部数组和引用对象才不拷贝,其他原始类型比如int、long、char等都会拷贝,但是String类型呢,java就希望把String类型认为是基本类型,它是没有clone方法的,处理机制也比较特殊,通过字符串池(stringpool)在需要的时候才在内存中创建新的字符串,在使用的时候可以把String类型当基本类型使用即可。
注意 使用原型模式时,引用成员变量必须满足两个条件才不会被拷贝:一是类的成员变量,而不是方法内变量;二是必须是一个可变的引用变量,而不是一个原始类型或不可变对象。
浅拷贝是有风险的,那么怎么才能深入拷贝呢?我们修改一下程序就可以深拷贝了,代码如下:
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;
}
只需要在clone的方法中对私有变量进行独立的拷贝就可以了。
该方法就实现了完全拷贝,两个对象之间没有任何的瓜葛了,你修改你的,我修改我的,不互相影响,这种拷贝就叫做深拷贝。深拷贝还有一种实现方式就是通过自己写二进制流俩操作对象,然后时间对象的深拷贝(这种方式书上没有写例子,需要百度)
注意 深拷贝和浅拷贝建议不要混合使用,特别是在涉及类的继承时,父类有多个引用的情况就非常复杂了,建议方案是深拷贝和浅拷贝分开实现。
4.3 clone与final两个冤家
对象的clone和对象内的final是有冲突的,你给成员变量添加final关键字,然后编译器就会报错,final类型是不让重新赋值的,所以说要使用clone就不能在成员变量上加final关键字,报错如图。
注意 要使用clone方法,类的成员变量不要增加final关键字。
-
最佳实践
原型模式先产生一个包含大量共有信息的类,然后拷贝出副本,修正细节信息,建立一个完整的个性对象。(我们之前学过工厂方法模式和建造者模式也是用来生产对象实例,但是好歹有个生产规则,根据指定的规则来生产实例;这个原型模式更简单粗暴,只要有了一个实例,直接在此基础上复制粘贴,在修改一些细节,简直深得复制粘贴的精髓,只要你有的我都可以有,哈哈)
内容来之《设计模式之禅》