案例展示——Prototype怎么用?
原型模式在开发过程中或许名头没有单例模式,工厂方法模式等设计模式那么响亮,但却是一个应用场景很多的设计模式,下面就通过一个案例来了解这个设计模式是什么。
随着互联网时代的来临,百花齐放,百家争鸣,各种各样的业务都依托互联网而发展的越来越繁荣。而诸多电信公司无疑是互联网产业中属于风口浪尖的产业,因为它们掌控互联网中最核心的源动力——流量。虽然卖流量卖话费已经让诸多电信公司赚得盆满泵满,但是哪个公司又会觉得赚得多呢?这不,随着邮件业务的兴起,越来越多的人喜欢上了这种快捷方便的交流方式,于是,各电信公司的目光都瞄上这个东西:
-
可不可以依托这个平台干点啥?
-
我数据库里面那么多人,平常节假日什么的做个活动啊什么的可不可以发一封广告邮件给他?
看看,业务就这样产生了。所以说需求这种东西都是铜臭味下的产物,没有什么业务是钱刺激不出来的。现在来考虑具体场景吧:
-
批量给数据库中业务对象发送广告邮件,并且每封邮件都需要进行私人定制:比如称谓是业务对象的名字。
-
批量发送邮件可能会被反垃圾邮件引擎误认为是垃圾邮件,所以需要在头部加一些伪造数据。
根据以上需求,可以得到设计类图如下:
-
Mail类是一个业务对象,包含了发一封邮件所需要的基本信息,给用户发送
-
AdvTemplate是广告信的模板
如下是代码实现:
//广告信模板
public class AdvTemplate {
//广告信名称
private String advSubject = "xx电信公司国庆充话费送手机活动";
//广告信内容
private String advContext = "国庆期间充话费满500元送一部华为手机!";
//取得广告信名称
public String getAdvSubject() {
return this.advSubject;
}
//取得广告信内容
public String getAdvContext() {
return this.advContext;
}
}
//mail业务类
public class Mail {
//收件人
private String receiver;
//邮件名称
private String subject;
//称谓
private String application;
//邮件内容
private String context;
//邮件尾部,例如:xx版权所有
private String tail;
public Mail(AdvTemplate advTemplate) {
this.context = advTemplate.getAdvContext();
this.subject = advTemplate.getAdvSubject();
}
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getApplication() {
return application;
}
public void setApplication(String application) {
this.application = application;
}
public String getContext() {
return context;
}
public void setContext(String context) {
this.context = context;
}
public String getTail() {
return tail;
}
public void setTail(String tail) {
this.tail = tail;
}
}
在一个场景中运行实现得:
public class Client {
//发送邮件的数量
private static int MAX_COUNT = 3;
public static void main(String[] args) {
int i = 0;
//定义模板
Mail mail = new Mail(new AdvTemplate());
//模拟发送场景
while (i < MAX_COUNT) {
mail.setApplication(getRandomString(5) + " 先生(女士) ");
mail.setReceiver(getRandomString(5) + "@" + getRandomString(8) + ".com");
sendMail(mail);
i++;
}
}
//发送邮件
public static void sendMail(Mail mail) {
System.out.println("标题:" + mail.getSubject() + "\t收件人:" + mail.getReceiver() + "\t...发送成功!");
}
//获得指定长度的随机字符串
public static String getRandomString(int maxLength) {
String source = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuffer sb = new StringBuffer();
Random random = new Random();
for (int i = 0; i < maxLength; i++) {
sb.append(source.charAt(random.nextInt(source.length())));
}
return sb.toString();
}
}
//结果如下:
标题:xx电信公司国庆充话费送手机活动 收件人:[email protected] ...发送成功!
标题:xx电信公司国庆充话费送手机活动 收件人:[email protected] ...发送成功!
标题:xx电信公司国庆充话费送手机活动 收件人:[email protected] ...发送成功!
看到实现结果,长舒了一口气,终于实现了。可是,仔细观察一下我们的设计,会发现存在很大的问题:程序是单线程的,批量发送耗时太长,假如每天要给600万用户发送邮件,按照一封0.02秒计算,需要33小时。。。这样的话,老板可能会让你打包走人了。也许可以在sendMail()上使用多线程,但是会出现线程安全问题,于是为了规避线程安全问题进行各种并发操作估计还是达不到要求。难道没有解决之法了?当然不是,原型模式闪亮登场啦,看下面的设计:
乍一看可能感觉和第一个设计没啥区别,无非不就是多了一个Cloneable接口和clone()方法吗?可这就是关键所在(克隆,有没有触碰到你的知识盲区啊?),既然创建一个对象很慢,为啥不只创建一个,然后在内存中直接复制出来呢,然后进行局部修改就可以了。
下面是代码实现与结果:
public class AdvTemplate {
//广告信名称
private String advSubject = "xx电信公司国庆充话费送手机活动";
//广告信内容
private String advContext = "国庆期间充话费满500元送一部华为手机!";
//取得广告信名称
public String getAdvSubject() {
return this.advSubject;
}
//取得广告信内容
public String getAdvContext() {
return this.advContext;
}
}
public class Mail implements Cloneable {
//收件人
private String receiver;
//邮件名称
private String subject;
//称谓
private String application;
//邮件内容
private String context;
//邮件尾部,例如:xx版权所有
private String tail;
public Mail(AdvTemplate advTemplate) {
this.context = advTemplate.getAdvContext();
this.subject = advTemplate.getAdvSubject();
}
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getApplication() {
return application;
}
public void setApplication(String application) {
this.application = application;
}
public String getContext() {
return context;
}
public void setContext(String context) {
this.context = context;
}
public String getTail() {
return tail;
}
public void setTail(String tail) {
this.tail = tail;
}
@Override
public Mail clone() {
Mail mail = null;
try {
mail = (Mail) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return mail;
}
}
public class Client {
//发送邮件的数量
private static int MAX_COUNT = 8;
public static void main(String[] args) {
int i = 0;
//定义模板
Mail mail = new Mail(new AdvTemplate());
//模拟发送场景
while (i < MAX_COUNT) {
Mail cloneMail = mail.clone();
cloneMail.setApplication(getRandomString(5) + " 先生(女士) ");
cloneMail.setReceiver(getRandomString(5) + "@" + getRandomString(8) + ".com");
sendMail(cloneMail);
i++;
}
}
//发送邮件
public static void sendMail(Mail mail) {
System.out.println("标题:" + mail.getSubject() + "\t收件人:" + mail.getReceiver() + "\t...发送成功!");
}
//获得指定长度的随机字符串
public static String getRandomString(int maxLength) {
String source = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuffer sb = new StringBuffer();
Random random = new Random();
for (int i = 0; i < maxLength; i++) {
sb.append(source.charAt(random.nextInt(source.length())));
}
return sb.toString();
}
}
//结果如下:
标题:xx电信公司国庆充话费送手机活动 收件人:[email protected] ...发送成功!
标题:xx电信公司国庆充话费送手机活动 收件人:[email protected] ...发送成功!
标题:xx电信公司国庆充话费送手机活动 收件人:[email protected] ...发送成功!
这种不通过new关键字来产生一个对象,而是通过对象复制来实现的模式叫做原型模式。
深入分析——Prototype是什么?
Prototype的定义
定义: 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式的通用类图如下:
原型模式的核心是clone()方法,通过该方法进行对象的拷贝,Java提供了一个Cloneable接口来表示这个对象是可拷贝的,Cloneable接口中没有任何方法,只是作为一种标记,只有实现了该接口的对象才会被JVM拷贝。
原型模式通用源码如下;
public class PrototypeClass implements Cloneable {
//重写父类Object方法
@Override
public PrototypeClass clone() {
PrototypeClass p = null;
try {
p = (PrototypeClass)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return p;
}
}
关于clone可以参考:https://blog.csdn.net/king123456man/article/details/81740255
Prototype延伸
Prototype的注意事项
注意: 一个实现了Cloneable接口并重写了clone()方法的类,先创建一个对象,然后通过这个对象调用clone()方法进行复制,在对象复制的时候该类的构造器不会执行。
Object类的clone()方法的原理是从内存中(堆内存)以二进制流的方式进行拷贝,重新分配一个内存块。而构造器用于初始化时是在堆内存中开辟空间创建对象的时候,拷贝并不是对象的完整创建过程,从这里理解的话,不应该执行构造器。
Prototype的优点
-
性能优良:原型模式是在内存中拷贝二进制流,要比直接new一个对象好得多,特别是在一个循环体内产生大量对象时,原型模式能更好的体现其优点。
-
逃避构造函数的约束:直接在内存中拷贝,不会执行构造函数,减少了约束
Prototype的使用场景
-
资源优化场景:类初始化需要消耗非常多的资源,所以在需要使用大量同一对象的时候可以考虑原型模式
-
性能和安全要求的场景:通过new产生一个对象要非常繁琐的数据准备或访问权限,可以使用原型模式替代
-
一个对象多个修改者场景:一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改该对象的值,可以使用原型模式拷贝多个对象供调用者使用
在项目开发中,原型模式一般和工厂方法模式出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。
参考
《设计模式之禅》