23種設(shè)計(jì)模式-原型模式

  1. 個(gè)性電子賬單

現(xiàn)在電子賬單越來(lái)越流行了,比如你的信用卡,沒(méi)到月初的時(shí)候銀行就會(huì)發(fā)一份電子郵件給你,說(shuō)你這個(gè)月消費(fèi)了多少,什么時(shí)候消費(fèi)的,積分是多少等,這是每個(gè)月發(fā)一次的。還有就是各大銀行發(fā)的廣告信,雖然電子郵件的模板大致都相同,但是有些地方是有區(qū)別的,就是客戶的稱呼,銀行發(fā)送該類(lèi)郵件是有要求的:

  • 個(gè)性化服
    一般銀行都要求個(gè)性化服務(wù),發(fā)過(guò)去的郵件上總有一些個(gè)人信息,比如"xx先生","xx女士"等。
  • 遞送成功率
    郵件的遞送成功率是有一定得要求,由于大批量地發(fā)送郵件會(huì)被接收方郵件服務(wù)器誤認(rèn)是垃圾郵件,因此要在郵件頭要增加一些偽造數(shù)據(jù),以規(guī)避被反垃圾郵件引擎誤認(rèn)為是垃圾郵件。
    從這兩方面考慮廣告信的發(fā)送也是電子賬單系統(tǒng)(電子賬單系統(tǒng)一般包括:賬單分析、廣告信息管理、發(fā)送隊(duì)列管理、發(fā)送機(jī)、退信處理、報(bào)表管理等)的一個(gè)子功能,我們今天就倆考慮一下廣告信這個(gè)模塊是怎么開(kāi)發(fā)的。那既然是廣告信,肯定要一個(gè)模板,然后在從數(shù)據(jù)庫(kù)中把客戶的信息一個(gè)個(gè)的取出來(lái),放到模板中生成一份完整的郵件,然后扔給發(fā)送機(jī)進(jìn)行處理,類(lèi)圖13-1:


    13-1

    在類(lèi)圖中AdvTemplate是廣告信息的模板,一般都是從數(shù)據(jù)庫(kù)中取出,生成一個(gè)BO或是DTO,我們這里使用一個(gè)靜態(tài)值做代表;Mail是一封郵件類(lèi),發(fā)送機(jī)發(fā)送的就是這個(gè)類(lèi)。我們先來(lái)看AdvTemplate和Mail,代碼如下:

public class AdvTemplate {
    private String advSubject = "xx銀行國(guó)慶信用卡抽獎(jiǎng)活動(dòng)";
    private String advContext = "國(guó)慶抽獎(jiǎng)活動(dòng)通知;只要刷卡就送你一百萬(wàn)!...";
    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就是一個(gè)業(yè)務(wù)對(duì)象,雖然比較長(zhǎng)。我們?cè)诳纯礃I(yè)務(wù)場(chǎng)景類(lèi)是如何對(duì)郵件進(jìn)行處理的,代碼如下:

public class Client {
    //發(fā)送賬單的數(shù)據(jù)量,這個(gè)值是從數(shù)據(jù)庫(kù)中獲得
    private static int MAX_COUNT = 6;
    public static void main(String[] args){
        //模擬發(fā)送郵件
        int i=0;
        //把模板定義出來(lái),這個(gè)是從數(shù)據(jù)庫(kù)中獲得
        Mail mail = new Mail(new AdvTemplate());
        mail.setTail("xx銀行版權(quán)所有");
        while(i<MAX_COUNT){
            //以下是每封郵件不同的地方
            mail.setAppellation(getRandString(5)+"先生(女士)");
            mail.setRecevier(getRandString(5)+"@"+getRandString(8)+".com");
            sendMail(mail);
            i++;
        }
    }
 //發(fā)送郵件
    public static void sendMail(Mail mail){
        System.out.println("標(biāo)題:"+mail.getSubject()+"\t收件人;"+mail.getRecevier()+"\t...發(fā)送成功!");
    }

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

}

發(fā)送郵件一般都是這么做法,我們仔細(xì)想想,這個(gè)程序是否有問(wèn)題?這是一個(gè)線程在運(yùn)行,那按照發(fā)送一封郵件需要0.02秒(夠小了,你還要到數(shù)據(jù)庫(kù)中取數(shù)據(jù)呢),600萬(wàn)封郵件需要33小時(shí),也就是一個(gè)整天都發(fā)送不完,今天的沒(méi)發(fā)送完,明天的賬單又產(chǎn)生了,日積月累,激起甲方人員一堆抱怨,那怎么辦?那我們報(bào)sendMail修改為多線程,但是只把sendMail修改為多線程還是有問(wèn)題呀,產(chǎn)生第一封郵件對(duì)象,放到線程1中運(yùn)行,還沒(méi)有發(fā)送出去;線程2也啟動(dòng)了,直接就把郵件對(duì)象mail的收件人地址和稱謂修改掉了,線程不安全了(一般多線程就不是怎么個(gè)寫(xiě)法了,就不會(huì)把mail在這地方去new了,但是這里mail是模板,我們且往下面看)。說(shuō)道這里,你會(huì)說(shuō)這有N種解決辦法,其中一種是使用一種新型模式來(lái)解決這個(gè)問(wèn)題:通過(guò)對(duì)象的復(fù)制功能來(lái)解決這個(gè)問(wèn)題,類(lèi)圖稍作修改,如圖13-2所示:

13-2

增加了一個(gè)Cloneable接口(java自帶的一個(gè)接口),Mail實(shí)現(xiàn)了這個(gè)接口,在Mail類(lèi)中覆寫(xiě)clone()方法,我們來(lái)看Mail類(lèi)的改變,代碼如下:

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

注意看粗體部分,實(shí)現(xiàn)一個(gè)接口,并重寫(xiě)了clone方法,大家可能看著這個(gè)類(lèi)有點(diǎn)奇怪,先保留你的好奇,我們繼續(xù)講下去,稍后會(huì)給你清晰的答案。我們?cè)賮?lái)看場(chǎng)景Client的變化,代碼如下:

public class Client {
    //發(fā)送賬單的數(shù)據(jù)量,這個(gè)值是從數(shù)據(jù)庫(kù)中獲得
    private static int MAX_COUNT = 6;
    public static void main(String[] args){
        //模擬發(fā)送郵件
        int i=0;
        //把模板定義出來(lái),這個(gè)是從數(shù)據(jù)庫(kù)中獲得
        Mail mail = new Mail(new AdvTemplate());
        mail.setTail("xx銀行版權(quán)所有");
        while(i<MAX_COUNT){
            //以下是每封郵件不同的地方
            Mail cloneMail = mail.clone();
            cloneMail.setAppellation(getRandString(5)+"先生(女士)");
            cloneMail.setRecevier(getRandString(5)+"@"+getRandString(8)+".com");
            sendMail(cloneMail);
            i++;
        }
    }
 //發(fā)送郵件
    public static void sendMail(Mail mail){
        System.out.println("標(biāo)題:"+mail.getSubject()+"\t收件人;"+mail.getRecevier()+"\t...發(fā)送成功!");
    }

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

一樣完成了電子廣告的發(fā)送,而且sendMail即使是多線程也沒(méi)有關(guān)系(這里沒(méi)有用多線程寫(xiě))。注意,看Client中的粗體字mail.clone()這個(gè)方法,把對(duì)象復(fù)制一份,產(chǎn)生一個(gè)新的對(duì)象,和原有對(duì)象一樣,然后在修改細(xì)節(jié)的數(shù)據(jù),如設(shè)置稱謂、收件人地址等,這種不通過(guò)new關(guān)鍵字來(lái)產(chǎn)生一個(gè)對(duì)象,而是通過(guò)對(duì)象復(fù)制來(lái)實(shí)現(xiàn)的模式就叫做原型模式。(這個(gè)地方用到了java提供的并發(fā)包中兩個(gè)很重要的思想之一叫做寫(xiě)時(shí)復(fù)制,這是保證處理高并發(fā)時(shí)一種線程安全的做法

  1. 原型模式的定義

原型模式(Prototype Pattern)的簡(jiǎn)單程度僅次于單例模式和迭代器模式。正是由于簡(jiǎn)單,使用的場(chǎng)景才非常的多,其定義如下:
Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.(用原型實(shí)例指定創(chuàng)建對(duì)象的種類(lèi),并且通過(guò)拷貝這些原型創(chuàng)建新的對(duì)象。)

原型模式的通用類(lèi)圖13-3:


13-3

原型模式的核心是一個(gè)clone方法,通過(guò)該方法進(jìn)行對(duì)象的拷貝,java提供了一個(gè)cloneable接口來(lái)標(biāo)識(shí)這個(gè)對(duì)象是可拷貝的,這個(gè)接口沒(méi)有方法,只是標(biāo)記作用,在jvm中具有這個(gè)標(biāo)記的對(duì)象才有可能被拷貝。那怎么才能從"有可能被拷貝"轉(zhuǎn)換為"可以被拷貝呢"?方法是覆蓋clone()方法,看看我們上面Mail類(lèi)中的clone方法,在clone()方法中增加了一個(gè)注解@Override,這個(gè)是覆寫(xiě)的Object的clone方法!我們來(lái)看看原型模式的通用源碼,代碼如下:

public class PrototypeClass implements Cloneable{
    @Override
    public PrototypeClass clone(){
        PrototypeClass prototypeClass = null;
        try {
            prototypeClass = (PrototypeClass)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return prototypeClass;
    }
}

實(shí)現(xiàn)一個(gè)接口Cloneable,然后重寫(xiě)clone方法,就完成了一個(gè)原型模式!(其實(shí)在這里有一個(gè)疑問(wèn),這個(gè)clone方法里面為什么使用的super.clone(),我百度了一下沒(méi)看到很好的解釋,我是這樣理解的,可以看Object中的clone方法有一個(gè)關(guān)鍵詞natice,表示這是個(gè)本地方法調(diào)用的低層代碼,然后我們覆寫(xiě)的clone方法實(shí)際比沒(méi)有實(shí)現(xiàn)復(fù)制對(duì)象的邏輯,所以必須使用super.clone來(lái)調(diào)用Object的clone方法;不知道對(duì)不對(duì)哈

  1. 原型模式的應(yīng)用

3.1 原型模式的優(yōu)點(diǎn)

  • 性能優(yōu)良
    原型模式是在內(nèi)存二進(jìn)制流的拷貝,要比直接new一個(gè)對(duì)象性能好很多,特別是要在一個(gè)循環(huán)體內(nèi)產(chǎn)生大量對(duì)象時(shí),原型模式可以更好地體現(xiàn) 其優(yōu)點(diǎn)。
  • 逃避構(gòu)造函數(shù)的約束
    這既是它的有點(diǎn)也是缺點(diǎn),直接在內(nèi)存中拷貝,構(gòu)造函數(shù)是不會(huì)執(zhí)行的。優(yōu)點(diǎn)就是減少了約束,缺點(diǎn)也是減少了約束,需要大家在實(shí)際應(yīng)用時(shí)考慮。(這個(gè)只能說(shuō)遇見(jiàn)了才比較清楚是怎么回事,我目前還沒(méi)見(jiàn)過(guò),實(shí)際上我開(kāi)發(fā)中用到的clone的都比較少,clone也是有很多限制的,不小心就很容易出現(xiàn)問(wèn)題
    3.2 原型模式的使用場(chǎng)景
  • 資源優(yōu)化場(chǎng)景
    類(lèi)初始化需要消化非常多的資源,這個(gè)資源包括數(shù)據(jù)、硬件資源等
  • 性能和安全要求的場(chǎng)景
    通過(guò)new產(chǎn)生一個(gè)對(duì)象需要非常繁瑣的數(shù)據(jù)準(zhǔn)備或訪問(wèn)權(quán)限,則可以使用原型模式。
  • 一個(gè)對(duì)象多個(gè)修改者的場(chǎng)景
    一個(gè)對(duì)象需要提供給其他對(duì)象訪問(wèn),而且各個(gè)調(diào)用者可能都需要修改其值時(shí),可以考慮使用原型模式拷貝多個(gè)對(duì)象供調(diào)用者使用。
    在實(shí)際項(xiàng)目中,原型模式很少單獨(dú)出現(xiàn),一般是和工廠方法模式一起出現(xiàn),通過(guò)clone的方法創(chuàng)建一個(gè)對(duì)象,然后由工廠方法提供給調(diào)用者。原型模式已經(jīng)與java融為一體,大家可以隨手哪來(lái)使用。
  1. 原型模式的注意事項(xiàng)

原型模式雖然簡(jiǎn)單,但是在java中使用使用原型模式也就是clone方法還是有一些注意事項(xiàng)的,通過(guò)例子學(xué)習(xí)。
4.1構(gòu)造函數(shù)不會(huì)被執(zhí)行
一個(gè)實(shí)現(xiàn)了Cloneable并重寫(xiě)了clone方法的類(lèi)A,有一個(gè)無(wú)參構(gòu)造或有參構(gòu)造B,通過(guò)new 關(guān)鍵字產(chǎn)生一個(gè)對(duì)象S,在然后通過(guò)S.clone()方式產(chǎn)生了一個(gè)新的對(duì)象T,那么在對(duì)象拷貝時(shí)構(gòu)造函數(shù)B是不會(huì)被執(zhí)行的,通過(guò)代碼來(lái)看:

public class Client {
    public static class Thing implements Cloneable{
        public Thing(){
            System.out.println("構(gòu)造函數(shù)被執(zhí)行了。。。。");
        }
        @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);
    }

}

對(duì)象拷貝時(shí)構(gòu)造函數(shù)確實(shí)沒(méi)有被執(zhí)行,這點(diǎn)從原理上也是講的通的,Object類(lèi)的clone方法的原理是從內(nèi)存中(具體說(shuō)的就是堆內(nèi)存)以二進(jìn)制流的方式進(jìn)行拷貝,重新分配一個(gè)內(nèi)存塊,那構(gòu)造函數(shù)沒(méi)有被執(zhí)行也是正常的了。
4.2 淺拷貝和深拷貝
在學(xué)習(xí)淺拷貝和深拷貝之前我們先看個(gè)例子,代碼如下:

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;
        }
        //設(shè)置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());
    }
}

//運(yùn)行結(jié)果
[zhangsan, lisi]

為什么會(huì)有l(wèi)isi呢?是因?yàn)閖ava做了一個(gè)偷懶的拷貝動(dòng)作,Object類(lèi)提供的方法clone只是拷貝本對(duì)象,其對(duì)象內(nèi)部數(shù)組、引用對(duì)象等都不拷貝,還是指向原生對(duì)象的內(nèi)部元素地址,這種拷貝就叫做淺拷貝。確實(shí)是非常淺兩個(gè)對(duì)象共享了一個(gè)私有變量,你改我改大家改,是一種非常不安全的方式,在實(shí)際項(xiàng)目中使用的還是比較少的(當(dāng)然,這也是一種"危機(jī)"環(huán)境的一種救命方式)。為什么在Mail那個(gè)類(lèi)中就可以使用String類(lèi)型,而不會(huì)產(chǎn)生由淺拷貝帶來(lái)的問(wèn)題呢??jī)?nèi)部數(shù)組和引用對(duì)象才不拷貝,其他原始類(lèi)型比如int、long、char等都會(huì)拷貝,但是String類(lèi)型呢,java就希望把String類(lèi)型認(rèn)為是基本類(lèi)型,它是沒(méi)有clone方法的,處理機(jī)制也比較特殊,通過(guò)字符串池(stringpool)在需要的時(shí)候才在內(nèi)存中創(chuàng)建新的字符串,在使用的時(shí)候可以把String類(lèi)型當(dāng)基本類(lèi)型使用即可。

注意 使用原型模式時(shí),引用成員變量必須滿足兩個(gè)條件才不會(huì)被拷貝:一是類(lèi)的成員變量,而不是方法內(nèi)變量;二是必須是一個(gè)可變的引用變量,而不是一個(gè)原始類(lèi)型或不可變對(duì)象。

淺拷貝是有風(fēng)險(xiǎn)的,那么怎么才能深入拷貝呢?我們修改一下程序就可以深拷貝了,代碼如下:

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的方法中對(duì)私有變量進(jìn)行獨(dú)立的拷貝就可以了。
該方法就實(shí)現(xiàn)了完全拷貝,兩個(gè)對(duì)象之間沒(méi)有任何的瓜葛了,你修改你的,我修改我的,不互相影響,這種拷貝就叫做深拷貝。深拷貝還有一種實(shí)現(xiàn)方式就是通過(guò)自己寫(xiě)二進(jìn)制流倆操作對(duì)象,然后時(shí)間對(duì)象的深拷貝(這種方式書(shū)上沒(méi)有寫(xiě)例子,需要百度
注意 深拷貝和淺拷貝建議不要混合使用,特別是在涉及類(lèi)的繼承時(shí),父類(lèi)有多個(gè)引用的情況就非常復(fù)雜了,建議方案是深拷貝和淺拷貝分開(kāi)實(shí)現(xiàn)。

4.3 clone與final兩個(gè)冤家
對(duì)象的clone和對(duì)象內(nèi)的final是有沖突的,你給成員變量添加final關(guān)鍵字,然后編譯器就會(huì)報(bào)錯(cuò),final類(lèi)型是不讓重新賦值的,所以說(shuō)要使用clone就不能在成員變量上加final關(guān)鍵字,報(bào)錯(cuò)如圖。


finalError

注意 要使用clone方法,類(lèi)的成員變量不要增加final關(guān)鍵字。

  1. 最佳實(shí)踐

原型模式先產(chǎn)生一個(gè)包含大量共有信息的類(lèi),然后拷貝出副本,修正細(xì)節(jié)信息,建立一個(gè)完整的個(gè)性對(duì)象。(我們之前學(xué)過(guò)工廠方法模式和建造者模式也是用來(lái)生產(chǎn)對(duì)象實(shí)例,但是好歹有個(gè)生產(chǎn)規(guī)則,根據(jù)指定的規(guī)則來(lái)生產(chǎn)實(shí)例;這個(gè)原型模式更簡(jiǎn)單粗暴,只要有了一個(gè)實(shí)例,直接在此基礎(chǔ)上復(fù)制粘貼,在修改一些細(xì)節(jié),簡(jiǎn)直深得復(fù)制粘貼的精髓,只要你有的我都可以有,哈哈

內(nèi)容來(lái)之《設(shè)計(jì)模式之禪》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 類(lèi)似于《西游記》中的孫悟空拔出猴毛,根據(jù)自己的樣子變出很多猴子來(lái)。或者是《火影忍者》中鳴人使用影分身變出很多...
    soberbad閱讀 697評(píng)論 0 2
  • 作用:拷貝。和構(gòu)建者模式相似,構(gòu)建者幫助我們把精力放在復(fù)雜的可配置項(xiàng)上,而原型模式使用在創(chuàng)建復(fù)雜的或者構(gòu)造耗時(shí)的實(shí)...
    王靈閱讀 408評(píng)論 0 0
  • 個(gè)性化電子賬單 現(xiàn)在電子賬單越來(lái)越流行了,比如你的信用卡,到月初的時(shí)候銀行就會(huì)發(fā)一份電子郵件到你郵箱中,說(shuō)你這個(gè)月...
    WILL_HUNTING閱讀 513評(píng)論 0 0
  • 小靜香挎著籃子,跟著如雪姑娘緩緩的走向妙心坊。 “姑娘,張家夫人還算有羞花貌,為何心病如此重?” 靜香低頭在籃子里...
    非墨流白閱讀 359評(píng)論 2 5
  • 關(guān)于中西方文化;談?wù)撨@方面,我暫且不去解讀其中的內(nèi)容是什么,體量太龐大了。而是從它存在的意義來(lái)識(shí)別,中國(guó)文化歷史悠...
    張赟ZhangYun閱讀 156評(píng)論 0 0

友情鏈接更多精彩內(nèi)容