020.原型模式

我們今天來考慮一下給用戶郵箱發(fā)廣告信這個模塊是怎么開發(fā)的。既然是廣告信,肯定需要一個模版,然后再從數(shù)據(jù)庫中把客戶的信息一個一個的取出,放到模版中生成一份完整的郵件,然后扔給發(fā)送機(jī)進(jìn)行發(fā)送處理,我們來看類圖:

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

public class AdvTemplate {

    /**
     * 廣告信名稱
     */
    private String advSubject = "XX銀行國慶信用卡抽獎活動";

    /**
     * 廣告信內(nèi)容
     */
    private String advContext = "國慶抽獎活動通知:只要刷卡就送你1百萬!....";

    public String getAdvSubject() {
        return advSubject;
    }

    public String getAdvContext() {
        return advContext;
    }
}

public class Mail {

    /**
     * 收件人
     */
    private String receiver;

    /**
     * 主題
     */
    private String subject;

    /**
     * 稱呼
     */
    private String appellation;

    /**
     * 郵件內(nèi)容
     */
    private String context;

    /**
     * 郵件尾部信息
     */
    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 class Client {

    /**
     * 發(fā)送郵件的數(shù)量
     */
    private static int maxCount = 6;

    public static void main(String[] args) {

        // 模擬發(fā)送郵件
        int i = 0;
        // 定義模板
        Mail mail = new Mail(new AdvTemplate());
        mail.setTail("XX銀行版本所有");
        while (i < maxCount) {
            mail.setAppellation(getRandString(5) + " 先生/女士");
            mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
            sendMail(mail);
            i++;
        }
    }

    /**
     * 發(fā)送郵件
     */
    public static void sendMail(Mail mail) {
        System.out.println(String.format("標(biāo)題: %s, 收件人: %s ... 發(fā)送成功!", mail.getSubject(), mail.getReceiver()));
    }

    /**
     * 生成隨機(jī)字符串
     * @param maxLength 字符串的最大長度
     * @return 生成的字符串
     */
    public static String getRandString(int maxLength) {
        String source = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        for (int i = 0; i < maxLength; i++) {
            sb.append(source.charAt(random.nextInt(source.length())));
        }
        return sb.toString();
    }

}

程序?qū)懗鰜砹耍覀兛紤]一個問題:發(fā)郵件可以使用多線程去發(fā)嗎?當(dāng)然是可以的,但是會有線程安全的問題,產(chǎn)生第一封郵件對象,放到線程1中運行,還沒有發(fā)送出去;線程2也也啟動了,直接就把郵件對象mail的收件人地址和稱謂修改掉了,線程安全有多種解決辦法,我們這里使用原型模式來解決這個問題,使用對象的拷貝功能來解決這個問題,類圖稍作修改,如下圖:

我們來看Mail類的改變:

public class Mail implements Cloneable {

    /**
     * 收件人
     */
    private String receiver;

    /**
     * 主題
     */
    private String subject;

    /**
     * 稱呼
     */
    private String appellation;

    /**
     * 郵件內(nèi)容
     */
    private String context;

    /**
     * 郵件尾部信息
     */
    private String tail;

    public Mail(AdvTemplate advTemplate) {
        this.context = advTemplate.getAdvContext();
        this.subject = advTemplate.getAdvSubject();
    }

    @Override
    protected Mail clone() throws CloneNotSupportedException {
        return (Mail)super.clone();
    }

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

Client類的改變:

public class Client {

    /**
     * 發(fā)送郵件的數(shù)量
     */
    private static int maxCount = 6;

    public static void main(String[] args) throws Exception {

        // 模擬發(fā)送郵件
        int i = 0;
        // 定義模板
        Mail mail = new Mail(new AdvTemplate());
        mail.setTail("XX銀行版本所有");
        while (i < maxCount) {
            Mail cloneMail = mail.clone();
            cloneMail.setAppellation(getRandString(5) + " 先生/女士");
            cloneMail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
            sendMail(cloneMail);
            i++;
        }
    }

    /**
     * 發(fā)送郵件
     */
    public static void sendMail(Mail mail) {
        System.out.println(String.format("標(biāo)題: %s, 收件人: %s ... 發(fā)送成功!", mail.getSubject(), mail.getReceiver()));
    }

    /**
     * 生成隨機(jī)字符串
     * @param maxLength 字符串的最大長度
     * @return 生成的字符串
     */
    public static String getRandString(int maxLength) {
        String source = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        for (int i = 0; i < maxLength; i++) {
            sb.append(source.charAt(random.nextInt(source.length())));
        }
        return sb.toString();
    }

}

一樣完成了電子廣告信的發(fā)送功能,而且sendMail()即使是多線程也沒有關(guān)系,mail.clone()這個方法把對象拷貝一份,產(chǎn)生一個新的對象,和原有對象一樣,然后再修改細(xì)節(jié)的數(shù)據(jù),如設(shè)置稱謂,設(shè)置收件人地址等等。這種不通過new關(guān)鍵字來產(chǎn)生一個對象,而是通過對象拷貝來實現(xiàn)的模式就叫做原型模式,其通用類圖如下:

這個模式的核心是一個clone()方法,通過這個方法進(jìn)行對象的拷貝,Java提供了一個Cloneable接口來標(biāo)示這個對象是可拷貝的,為什么說是“標(biāo)示”呢?翻開JDK的幫助看看Cloneable是一個方法都沒有的,這個接口只是一個標(biāo)記作用,在JVM中具有這個標(biāo)記的對象才有可能被拷貝,那怎么才能從“有可能被拷貝”轉(zhuǎn)換為“可以被拷貝”呢?方法是覆蓋clone()方法。

原型模式雖然很簡單,但是在Java中使用原型模式也就是clone()方法還是有一些注意事項的:

  • 對象拷貝時,類的構(gòu)造函數(shù)是不會被執(zhí)行的,對象拷貝時確實構(gòu)造函數(shù)沒有被執(zhí)行,這個從原理來講也是可以講得通的,Object類的clone()方法的原理是從推內(nèi)存中以二進(jìn)制流的方式進(jìn)行拷貝,重新分配一個內(nèi)存塊,那構(gòu)造函數(shù)沒有被執(zhí)行也是非常正常的了。

    public class CloneExample implements Cloneable {
    
        public CloneExample() {
            System.out.println("調(diào)用構(gòu)造器...");
        }
    
        @Override
        protected CloneExample clone() throws CloneNotSupportedException {
            return (CloneExample)super.clone();
        }
    
        public static void main(String[] args) throws Exception {
    
            CloneExample ce1 = new CloneExample();
            CloneExample ce2 = ce1.clone();
            System.out.println(ce1);
            System.out.println(ce2);
    
        }
    }
    
  • 淺拷貝和深拷貝問題

    public class CloneExample2 implements Cloneable {
    
        private ArrayList<String> arrayList = new ArrayList<>();
    
        @Override
        protected CloneExample2 clone() throws CloneNotSupportedException {
            return (CloneExample2)super.clone();
        }
    
        public void setValue(String value) {
            arrayList.add(value);
        }
    
        public ArrayList<String> getValue() {
            return arrayList;
        }
    
        public static void main(String[] args) throws Exception {
            CloneExample2 ce1 = new CloneExample2();
            ce1.setValue("張三");
            CloneExample2 ce2 = ce1.clone();
            ce2.setValue("李四");
            System.out.println(ce1.getValue()); // 結(jié)果是: [張三, 李四]
        }
    
    }
    

    怎么會有李四呢?是因為Java做了一個偷懶的拷貝動作,Object類提供的方法clone() 只是拷貝本對象,其對象內(nèi)部的數(shù)組、引用對象等都不拷貝,還是指向原生對象的內(nèi)部元素地址,這種拷貝就叫做淺拷貝,確實是非常淺,兩個對象共享了一個私有變量,你改我改大家都能改,是一個種非常不安全的方式,在實際項目中使用還是比較少的。你可能會比較奇怪,為什么在Mail那個類中就可以使用String類型,而不會產(chǎn)生由淺拷貝帶來的問題呢?內(nèi)部的數(shù)組和引用對象才不拷貝,其他的原始類型比如int、longString(Java就希望你把String認(rèn)為是基本類型,String是沒有clone()方法的)等都會被拷貝的。淺拷貝是有風(fēng)險的,那怎么才能深入的拷貝呢?我們修改一下我們的程序:

    @Override
    protected CloneExample2 clone() throws CloneNotSupportedException {
      /*
      * 淺拷貝
      * return (CloneExample2)super.clone();
      */
        /*
         * 深拷貝
         */
      CloneExample2 ce = (CloneExample2)super.clone();
      ce.arrayList = (ArrayList<String>)arrayList.clone();
      return ce;
    }
    

    深拷貝還有一種實現(xiàn)方式就是通過自己寫二進(jìn)制流來操作對象,然后實現(xiàn)對象的深拷貝,深拷貝和淺拷貝建議不要混合使用,一個類中某些引用使用深拷貝,某些引用使用淺拷貝,這是一種非常差的設(shè)計,特別是是在涉及到類的繼承,父類有幾個引用的情況就非常的復(fù)雜,建議的方案深拷貝和淺拷貝分開實現(xiàn)。

  • 對象的clone()與對象內(nèi)的final屬性是沖突的

    public class CloneExample3 implements Cloneable {
    
        private final ArrayList<String> arrayList = new ArrayList<>();
    
        @Override
        protected CloneExample3 clone() throws CloneNotSupportedException {
            CloneExample3 ce = (CloneExample3)super.clone();
            ce.arrayList = (ArrayList<String>)arrayList.clone(); // 編譯報錯
            return ce;
        }
    
        public void setValue(String value) {
            arrayList.add(value);
        }
    
        public ArrayList<String> getValue() {
            return arrayList;
        }
    }
    

原型模式的適用場景:

  • 一是類初始化需要消化非常多的資源,這個資源包括數(shù)據(jù)、硬件資源等;
  • 二是通過new產(chǎn)生一個對象需要非常繁瑣的數(shù)據(jù)準(zhǔn)備或訪問權(quán)限,則可以使用原型模式;
  • 三是一個對象需要提供給其他對象訪問,而且各個調(diào)用者可能都需要修改其值時,可以考慮使用原型模式拷貝多個對象供調(diào)用者使用。在實際項目中,原型模式很少單獨出現(xiàn),一般是和工廠方法模式一起出現(xiàn),通過clone()方法創(chuàng)建一個對象,然后由工廠方法提供給調(diào)用者。

本文原書:

《您的設(shè)計模式》 作者:CBF4LIFE

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

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

  • 原型模式定義:用原型實例指定創(chuàng)建對象的種類, 并且通過拷貝這些原型創(chuàng)建新的對象。 原型模式的核心是一個clone方...
    代碼墨白閱讀 109評論 0 0
  • 概念 在Java中實現(xiàn)原型模式十分簡單,只需要實現(xiàn)Cloneable接口并重寫clone()方法就可以了 Code...
    tanoak閱讀 686評論 0 0
  • 1、原型模式 1、定義 拷貝一個對象創(chuàng)建新的對象 2、使用場景 類初始化需要消耗非常多的資源; 通過new需要非常...
    Dane_404閱讀 159評論 0 0
  • 銀行發(fā)廣告信,為了提供個性化服務(wù),發(fā)過去的郵件需要帶上個人信息,如XX先生/小姐,又或者是電子賬單,這就需要一個模...
    涼快先生閱讀 430評論 0 1
  • 1大同小異的工作周報 Sunny軟件公司一直使用自行開發(fā)的一套OA (Office Automatic,辦公自動化...
    justCode_閱讀 1,246評論 0 3

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