23种设计模式-原型模式

  1. 个性电子账单

现在电子账单越来越流行了,比如你的信用卡,没到月初的时候银行就会发一份电子邮件给你,说你这个月消费了多少,什么时候消费的,积分是多少等,这是每个月发一次的。还有就是各大银行发的广告信,虽然电子邮件的模板大致都相同,但是有些地方是有区别的,就是客户的称呼,银行发送该类邮件是有要求的:

  • 个性化服
    一般银行都要求个性化服务,发过去的邮件上总有一些个人信息,比如"xx先生","xx女士"等。
  • 递送成功率
    邮件的递送成功率是有一定得要求,由于大批量地发送邮件会被接收方邮件服务器误认是垃圾邮件,因此要在邮件头要增加一些伪造数据,以规避被反垃圾邮件引擎误认为是垃圾邮件。
    从这两方面考虑广告信的发送也是电子账单系统(电子账单系统一般包括:账单分析、广告信息管理、发送队列管理、发送机、退信处理、报表管理等)的一个子功能,我们今天就俩考虑一下广告信这个模块是怎么开发的。那既然是广告信,肯定要一个模板,然后在从数据库中把客户的信息一个个的取出来,放到模板中生成一份完整的邮件,然后扔给发送机进行处理,类图13-1:


    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所示:

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提供的并发包中两个很重要的思想之一叫做写时复制,这是保证处理高并发时一种线程安全的做法

  1. 原型模式的定义

原型模式(Prototype Pattern)的简单程度仅次于单例模式和迭代器模式。正是由于简单,使用的场景才非常的多,其定义如下:
Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.(用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。)

原型模式的通用类图13-3:


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方法;不知道对不对哈

  1. 原型模式的应用

3.1 原型模式的优点

  • 性能优良
    原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量对象时,原型模式可以更好地体现 其优点。
  • 逃避构造函数的约束
    这既是它的有点也是缺点,直接在内存中拷贝,构造函数是不会执行的。优点就是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。(这个只能说遇见了才比较清楚是怎么回事,我目前还没见过,实际上我开发中用到的clone的都比较少,clone也是有很多限制的,不小心就很容易出现问题
    3.2 原型模式的使用场景
  • 资源优化场景
    类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等
  • 性能和安全要求的场景
    通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
  • 一个对象多个修改者的场景
    一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
    在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与java融为一体,大家可以随手哪来使用。
  1. 原型模式的注意事项

原型模式虽然简单,但是在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关键字,报错如图。


finalError

注意 要使用clone方法,类的成员变量不要增加final关键字。

  1. 最佳实践

原型模式先产生一个包含大量共有信息的类,然后拷贝出副本,修正细节信息,建立一个完整的个性对象。(我们之前学过工厂方法模式和建造者模式也是用来生产对象实例,但是好歹有个生产规则,根据指定的规则来生产实例;这个原型模式更简单粗暴,只要有了一个实例,直接在此基础上复制粘贴,在修改一些细节,简直深得复制粘贴的精髓,只要你有的我都可以有,哈哈

内容来之《设计模式之禅》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 前言 类似于《西游记》中的孙悟空拔出猴毛,根据自己的样子变出很多猴子来。或者是《火影忍者》中鸣人使用影分身变出很多...
    soberbad阅读 565评论 0 2
  • 作用:拷贝。和构建者模式相似,构建者帮助我们把精力放在复杂的可配置项上,而原型模式使用在创建复杂的或者构造耗时的实...
    王灵阅读 338评论 0 0
  • 个性化电子账单 现在电子账单越来越流行了,比如你的信用卡,到月初的时候银行就会发一份电子邮件到你邮箱中,说你这个月...
    WILL_HUNTING阅读 462评论 0 0
  • 小静香挎着篮子,跟着如雪姑娘缓缓的走向妙心坊。 “姑娘,张家夫人还算有羞花貌,为何心病如此重?” 静香低头在篮子里...
    非墨流白阅读 307评论 2 5
  • 关于中西方文化;谈论这方面,我暂且不去解读其中的内容是什么,体量太庞大了。而是从它存在的意义来识别,中国文化历史悠...
    张赟ZhangYun阅读 107评论 0 0