今天我们来讲原型模式,这个模式的简单程度是仅次于单例模式和迭代器模式,非常简单,但是要使用好这个模式还有很多注意事项。我们通过一个例子来解释一下什么是原型模式。

  现在电子账单越来越流行了,比如你的信用卡,到月初的时候银行就会发一份电子邮件到你邮箱中,说你这个月消费了多少,什么时候消费的,积分是多少等等,这个是每个月发一次,但是还有一种也是银行发的邮件你肯定有印象:广告信,现在各大银行的信用卡部门都在拉拢客户,电子邮件是一种廉价、快捷的通讯方式,你用纸质的广告信那个费用多高呀,比如我今天推出一个信用卡刷卡抽奖活动,通过电子账单系统可以一个晚上发送给 600 万客户,为什么要用电子账单系统呢?直接找个发垃圾邮件不就解决问题了吗?是个好主意,但是这个方案在金融行业是行不通的,银行发这种邮件是有要求的,一是一般银行都要求个性化服务,发过去的邮件上总有一些个人信息吧,比如“XX 先生”, “XX 女士”等等,二是邮件的到达率有一定的要求,由于大批量的发送邮件会被接收方邮件服务器误认是垃圾邮件,因此在邮件头要增加一些伪造数据,以规避被反垃圾邮件引擎误认为是垃圾邮件;从这两方面考虑广告信的发送也是电子账单系统(电子账单系统一般包括:账单分析、账单生成器、广告信管理、发送队列管理、发送机、退信处理、报表管理等)的一个子功能,我们今天就来考虑一下广告信这个模块是怎么开发的。那既然是广告信,肯定需要一个模版,然后再从数据库中把客户的信息一个一个的取出,放到模版中生成一份完整的邮件,然后扔给发送机进行发送处理,我们来看类图:

  在类图中 AdvTemplate 是广告信的模板,一般都是从数据库取出,生成一个 BO 或者是 DTO,我们这里使用一个静态的值来做代表;Mail 类是一封邮件类,发送机发送的就是这个类,我们先来看看我们的程序:

 package com.pattern.prototype;

 public class AdvTemplate {

     // 广告信名称
private String advSubject = "XX银行国庆节用卡抽奖活动"; // 广告信内容
private String advContext = "国庆抽象活动通知:只要刷卡就送你1百万!..."; // 取得广告信的名称
public String getAdvSubject(){
return this.advSubject;
} // 取得广告信的内容
public String getAdvContext(){
return this.advContext;
} } // 我们再来看邮件类: package com.pattern.prototype; public class Mail {
// 收件人
private String receiver;
// 邮件名称
private String subject;
// 称谓
private String appellation;
// 邮件内容
private String context;
// 邮件的尾部,一般都是加上"XXX版权所有"等信息
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 getAppellation() {
return appellation;
} public void setAppellation(String appellation) {
this.appellation = appellation;
} 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;
}
}

  Mail 就是一个业务对象,我们再来看业务场景类是怎么调用的:

 package com.pattern.prototype;

 import java.util.Random;

 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();
}
}

  由于是随机数,每次运行都由所差异,不管怎么样,我们这个电子账单发送程序时写出来了,也能发送出来了,我们再来仔细的想想,这个程序是否有问题?你看,你这是一个线程在运行,也就是你发送是单线程的, 那按照一封邮件发出去需要 0.02 秒(够小了,你还要到数据库中取数据呢), 600 万封邮件需要…我算算(掰指头计算中…),恩,是 33 个小时,也就是一个整天都发送不完毕,今天发送不完毕,明天的账单又产生了,积累积累,激起甲方人员一堆抱怨,那怎么办?

  好办,把 sendMail 修改为多线程,但是你只把 sendMail 修改为多线程还是有问题的呀,你看哦,产生第一封邮件对象,放到线程 1 中运行,还没有发送出去;线程 2 呢也也启动了,直接就把邮件对象 mail的收件人地址和称谓修改掉了,线程不安全了,好了,说到这里,你会说这有 N 多种解决办法,我们不多说,我们今天就说一种,使用原型模式来解决这个问题,使用对象的拷贝功能来解决这个问题,类图稍作修改,如下图:

  增加了一个 Cloneable 接口, Mail 实现了这个接口, 在 Mail 类中重写了 clone()方法, 我们来看 Mail类的改变:

 package com.pattern.prototype;

 public class Mail implements Cloneable {
// 收件人
private String receiver;
// 邮件名称
private String subject;
// 称谓
private String appellation;
// 邮件内容
private String context;
// 邮件的尾部,一般都是加上"XXX版权所有"等信息
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 getAppellation() {
return appellation;
} public void setAppellation(String appellation) {
this.appellation = appellation;
} 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 Mail clone(){
Mail mail = null;
try {
mail = (Mail) super.clone();
} catch (Exception e) {
e.printStackTrace();
}
return mail;
} }

  就做了一点修改,大家可能看着这个类有点奇怪,先保留你的好奇,我们继续讲下去,我会给你解答的,看 Client 类的改变:

 package com.pattern.prototype;

 import java.util.Random;

 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.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();
}
}

  执行结果不变,一样完成了电子广告信的发送功能,而且 sendMail 即使是多线程也没有关系,看到mail.clone()这个方法了吗?把对象拷贝一份,产生一个新的对象,和原有对象一样,然后再修改细节的数据,如设置称谓,设置收件人地址等等。这种不通过 new 关键字来产生一个对象,而是通过对象拷贝来实现的模式就叫做原型模式,其通用类图如下:

  这个模式的核心是一个 clone 方法,通过这个方法进行对象的拷贝,Java 提供了一个 Cloneable 接口来标示这个对象是可拷贝的,为什么说是“标示”呢?翻开 JDK 的帮助看看 Cloneable 是一个方法都没有的,这个接口只是一个标记作用,在 JVM 中具有这个标记的对象才有可能被拷贝,那怎么才能从“有可能被拷贝”转换为“可以被拷贝”呢?方法是覆盖 clone()方法,是的,你没有看错是重写 clone()方法,看看我们上面 Mail 类:

    

  在 clone()方法上增加了一个注解@Override, 没有继承一个类为什么可以重写呢?在 Java 中所有类的老祖宗是谁?对嘛,Object 类,每个类默认都是继承了这个类,所以这个用上重写是非常正确的。

  原型模式虽然很简单,但是在 Java 中使用原型模式也就是 clone 方法还是有一些注意事项的,我们通过几个例子一个一个解说(如果你对 Java 不是很感冒的话,可以跳开以下部分)。

  对象拷贝时,类的构造函数是不会被执行的。 一个实现了 Cloneable 并重写了 clone 方法的类 A,有一个无参构造或有参构造 B,通过 new 关键字产生了一个对象 S,再然后通过 S.clone()方式产生了一个新的对象 T,那么在对象拷贝时构造函数 B 是不会被执行的,我们来写一小段程序来说明这个问题:

 package com.pattern.prototype.clone_advance0;

 public class Thing implements Cloneable {

     public Thing(){
System.out.println("构造函数被执行了...");
} public Thing clone(){
Thing thing = null;
try {
thing = (Thing) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return thing;
} } // 然后我们再来写一个 Client 类,进行对象的拷贝: package com.pattern.prototype.clone_advance0; public class Client { public static void main(String[] args) {
// 产生一个对象
Thing thing = new Thing(); // 拷贝一个对象
Thing cloneThing = thing.clone();
System.out.println(cloneThing);
} }

  对象拷贝时确实构造函数没有被执行,这个从原理来讲也是可以讲得通的,Object 类的 clone 方法的原理是从内存中(具体的说就是堆内存)以二进制流的方式进行拷贝,重新分配一个内存块,那构造函数没有被执行也是非常正常的了。

  浅拷贝和深拷贝问题。 在解释什么是浅拷贝什么是拷贝前,我们先来看个例子:

 package com.pattern.prototype.clone_advance1;

 import java.util.ArrayList;

 public class Thing implements Cloneable {

     // 定义一个私有变量
private ArrayList<String> arrayList = new ArrayList<String>(); public Thing clone(){
Thing thing = null;
try {
thing = (Thing) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return thing;
} // 设置ArrayList的值
public void setValue(String value){
this.arrayList.add(value);
} // 取得arrayList的值
public ArrayList<String> getValue(){
return this.arrayList;
} }

  在 Thing 类中增加一个私有变量 arrayList,类型为 ArrayList,然后通过 setValue 和 getValue 分别进行设置和取值,我们来看场景类:

 package com.pattern.prototype.clone_advance1;

 public class Client {

     public static void main(String[] args) {
// 产生一个对象
Thing thing = new Thing();
// 设置一个值
thing.setValue("张三"); // 拷贝一个对象
Thing cloneThing = thing.clone();
cloneThing.setValue("李四"); System.out.println(thing.getValue());
} }

  大家猜想一下运行结果应该是什么?是就一个“张三”吗?

  怎么会有李四呢?是因为 Java 做了一个偷懒的拷贝动作, Object 类提供的方法 clone 只是拷贝本对象,其对象内部的数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址,这种拷贝就叫做浅拷贝,确实是非常浅,两个对象共享了一个私有变量,你改我改大家都能改,是一个种非常不安全的方式,在实际项目中使用还是比较少的。你可能会比较奇怪,为什么在 Mail 那个类中就可以使用使用 String 类型,而不会产生由浅拷贝带来的问题呢?内部的数组和引用对象才不拷贝,其他的原始类型比如int,long,String(Java 就希望你把 String 认为是基本类型, String 是没有 clone 方法的)等都会被拷贝的。

  浅拷贝是有风险的,那怎么才能深入的拷贝呢?我们修改一下我们的程序:

 package com.pattern.prototype.clone_advance3;

 import java.util.ArrayList;

 public class Thing implements Cloneable {

     // 定义一个私有变量
private ArrayList<String> arrayList = new ArrayList<String>(); @SuppressWarnings("unchecked")
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;
} }

  仅仅增加了一行部分,Client 类没有任何改变。

  这个实现了完全的拷贝,两个对象之间没有任何的瓜葛了,你修改你的,我修改我的,不相互影响,这种拷贝就叫做深拷贝,深拷贝还有一种实现方式就是通过自己写二进制流来操作对象,然后实现对象的深拷贝,这个大家有时间自己实现一下。

  深拷贝和浅拷贝建议不要混合使用,一个类中某些引用使用深拷贝某些引用使用浅拷贝,这是一种非常差的设计,特别是是在涉及到类的继承,父类有几个引用的情况就非常的复杂,建议的方案深拷贝和浅拷贝分开实现。

  Clone 与 final 两对冤家。 对象的 clone 与对象内的 final 属性是由冲突的, 我们举例来说明这个问题:

 package com.pattern.prototype.clone_advance3;

 import java.util.ArrayList;

 public class Thing implements Cloneable {

     // 定义一个私有变量
private final ArrayList<String> arrayList = new ArrayList<String>(); @SuppressWarnings("unchecked")
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;
} }

  上面的代码仅仅增加了一个 final 关键字,然后编译器就报错误,正常呀,final 类型你还想重写设值呀!完蛋了,你要实现深拷贝的梦想在 final关键字的威胁下破灭了,路总是有的,我们来想想怎么修改这个方法:删除掉 final 关键字,这是最便捷最安全最快速的方式,你要使用 clone 方法就在类的成员变量上不要增加 final 关键字。

  原型模式适合在什么场景使用?一是类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等;二是通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式;三是一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone的方法创建一个对象,然后由工厂方法提供给调用者。

  原型模式先产生出一个包含大量共有信息的类,然后可以拷贝出副本,修正细节信息,建立了一个完整的个性对象。不知道大家有没有看过施瓦辛格演的《第六日》这个电影,电影的主线也就是一个人被复制,然后正本和副本对掐,我们今天讲的原型模式也就是由一个正本可以创建多个副本的概念,可以这样理解一个对象的产生可以不由零开始,直接从一个已经具备一定雏形的对象克隆,然后再修改为一个生产需要的对象。也就是说,产生一个人,可以不从 1 岁长到 2 岁,再 3 岁…,也可以直接找一个人,从其身上获得 DNS,然后克隆一个,直接修改一下就是 3 岁的了!,我们讲的原型模式也就是这样的功能,是紧跟时代潮流的。

24种设计模式--原型模式【Prototype Pattern】的更多相关文章

  1. C#设计模式——原型模式(Prototype Pattern)

    一.概述 在软件开发中,经常会碰上某些对象,其创建的过程比较复杂,而且随着需求的变化,其创建过程也会发生剧烈的变化,但他们的接口却能比较稳定.对这类对象的创建,我们应该遵循依赖倒置原则,即抽象不应该依 ...

  2. 设计模式——原型模式(Prototype Pattern)

    原型模式:用原型实例制定创建对象的种类,并且通过拷贝这些原型创建新的对象. UML 图: 原型类: package com.cnblog.clarck; /** * 原型类 * * @author c ...

  3. 24种设计模式-策略模式(Strategy Pattern)

    一.优点: 1. 策略模式提供了管理相关的算法族的办法.策略类的等级结构定义了一个算法或行为族.恰当使用继承可以把公共的代码转移到父类里面,从而避免重复的代码. 2. 策略模式提供了可以替换继承关系的 ...

  4. 二十四种设计模式:原型模式(Prototype Pattern)

    原型模式(Prototype Pattern) 介绍用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象.示例有一个Message实体类,现在要克隆它. MessageModel usin ...

  5. Net设计模式实例之原型模式( Prototype Pattern)

    一.原型模式简介(Brief Introduction) 原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象. Specify the kin ...

  6. 设计模式系列之原型模式(Prototype Pattern)——对象的克隆

    说明:设计模式系列文章是读刘伟所著<设计模式的艺术之道(软件开发人员内功修炼之道)>一书的阅读笔记.个人感觉这本书讲的不错,有兴趣推荐读一读.详细内容也可以看看此书作者的博客https:/ ...

  7. 乐在其中设计模式(C#) - 原型模式(Prototype Pattern)

    原文:乐在其中设计模式(C#) - 原型模式(Prototype Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 原型模式(Prototype Pattern) 作者:weba ...

  8. PHP设计模式 原型模式(Prototype)

    定义 和工厂模式类似,用来创建对象.但实现机制不同,原型模式是先创建一个对象,采用clone的方式进行新对象的创建. 场景 大对象的创建. 优点 1.可以在运行时刻增加和删除产品 2.可以改变值或结构 ...

  9. 2.6 《硬啃设计模式》第8章 复制不是很难 - 原型模式(Prototype Pattern)

    案例: 某即时战略游戏,你训练出来各种很强的战士. 为了增加游戏的可玩性,增加了一种复制魔法.实施该魔法,可以复制任意的战士. 你会怎样考虑这个设计? 在继续阅读之前,请先认真思考并写出你的设计,这样 ...

随机推荐

  1. 潜语义分析(Latent Semantic Analysis)

    LSI(Latent semantic indexing, 潜语义索引)和LSA(Latent semantic analysis,潜语义分析)这两个名字其实是一回事.我们这里称为LSA. LSA源自 ...

  2. spring注入Properties

    最近项目中向将某个Properties注入到Bean中,经百度知以下代码. <bean id="settings" class="org.springframewo ...

  3. (Step by Step)How to setup IP Phone Server(VoIP Server) for free.

    You must have heard about IP Phone and SIP (Software IP Phone).Nowadays standard PSTN phone are bein ...

  4. SSDT – Error SQL70001 This statement is not recognized in this context-摘自网络

    March 28, 2013 — arcanecode One of the most common errors I get asked about when using SQL Server Da ...

  5. 分布式定时任务框架比较,spring batch, tbschedule jobserver

    分布式定时任务框架比较,spring batch, tbschedule jobserver | 移动开发参考书 分布式定时任务框架比较,spring batch, tbschedule jobser ...

  6. DOCTYPE与浏览器模式详解(标准模式&混杂模式)

    关于渲染模式: 在多年以前(IE6诞生以前),各浏览器都处于各自比较封闭的发展中(基本没有兼容性可谈).随着WEB的发展,兼容性问题的解决越来 越显得迫切,随即,各浏览器厂商发布了按照标准模式(遵循各 ...

  7. DAS 原文出自【比特网】

    http://www.360doc.com/content/13/1114/11/10504424_329109113.shtml

  8. Android开发书籍推荐

    当你看到这些文字时,那么恭喜你,你可能选择了一个无限可能的方向. Android,Google出品,信誉保证,你值得深入研究. 学习一样新事物或许有多种方式,报培训班,看视频,向高手请教等等,但一本好 ...

  9. C++ Code_combobox

    主题 1. 代码设置组合框风格 2. 调整组合框列表部分大小 3. 代码设置组合框相关属性 4. 自绘组合框 5. 用代码让combobox的的列表弹出 6. 不添加重复项目           代码 ...

  10. Linux下用来获取各种系统信息的C++类

    #include <vector> #include "sys/config.h" SYS_NAMESPACE_BEGIN /*** * 用来获取系统.内核和进程的各类 ...