1. 程式人生 > >(轉載)24種設計模式--原型模式【Prototype Pattern】

(轉載)24種設計模式--原型模式【Prototype Pattern】

dex clone() new t 分享圖片 object try arr 建立 不同

今天我們來講原型模式,這個模式的簡單程度是僅次於單例模式和叠代器模式,非常簡單,但是要使用好這個模式還有很多註意事項。我們通過一個例子來解釋一下什麽是原型模式。

  現在電子賬單越來越流行了,比如你的信用卡,到月初的時候銀行就會發一份電子郵件到你郵箱中,說你這個月消費了多少,什麽時候消費的,積分是多少等等,這個是每個月發一次,但是還有一種也是銀行發的郵件你肯定有印象:廣告信,現在各大銀行的信用卡部門都在拉攏客戶,電子郵件是一種廉價、快捷的通訊方式,你用紙質的廣告信那個費用多高呀,比如我今天推出一個信用卡刷卡抽獎活動,通過電子賬單系統可以一個晚上發送給 600 萬客戶,為什麽要用電子賬單系統呢?直接找個發垃圾郵件不就解決問題了嗎?是個好主意,但是這個方案在金融行業是行不通的,銀行發這種郵件是有要求的,一是一般銀行都要求個性化服務,發過去的郵件上總有一些個人信息吧,比如“XX 先生”, “XX 女士”等等,二是郵件的到達率有一定的要求,由於大批量的發送郵件會被接收方郵件服務器誤認是垃圾郵件,因此在郵件頭要增加一些偽造數據,以規避被反垃圾郵件引擎誤認為是垃圾郵件;從這兩方面考慮廣告信的發送也是電子賬單系統(電子賬單系統一般包括:賬單分析、賬單生成器、廣告信管理、發送隊列管理、發送機、退信處理、報表管理等)的一個子功能,我們今天就來考慮一下廣告信這個模塊是怎麽開發的。那既然是廣告信,肯定需要一個模版,然後再從數據庫中把客戶的信息一個一個的取出,放到模版中生成一份完整的郵件,然後扔給發送機進行發送處理,我們來看類圖:

技術分享圖片

  在類圖中 AdvTemplate 是廣告信的模板,一般都是從數據庫取出,生成一個 BO 或者是 DTO,我們這裏使用一個靜態的值來做代表;Mail 類是一封郵件類,發送機發送的就是這個類,我們先來看看我們的程序:

技術分享圖片
 1 package com.pattern.prototype;
 2 
 3 public class AdvTemplate {
 4     
 5     // 廣告信名稱
 6     private String advSubject = "XX銀行國慶節用卡抽獎活動";
 7     
 8     // 廣告信內容
 9     private String advContext = "國慶抽象活動通知:只要刷卡就送你1百萬!...";
10     
11     // 取得廣告信的名稱
12     public String getAdvSubject(){
13         return this.advSubject;
14     }
15     
16     // 取得廣告信的內容
17     public String getAdvContext(){
18         return this.advContext;
19     }
20     
21 }
22 
23 // 我們再來看郵件類:
24 
25 package com.pattern.prototype;
26 
27 public class Mail {
28     // 收件人
29     private String receiver;
30     // 郵件名稱
31     private String subject;
32     // 稱謂
33     private String appellation;
34     // 郵件內容
35     private String context;
36     // 郵件的尾部,一般都是加上"XXX版權所有"等信息
37     private String tail;
38 
39     // 構造函數
40     public Mail(AdvTemplate advTemplate) {
41         this.context = advTemplate.getAdvContext();
42         this.subject = advTemplate.getAdvSubject();
43     }
44 
45     public String getReceiver() {
46         return receiver;
47     }
48 
49     public void setReceiver(String receiver) {
50         this.receiver = receiver;
51     }
52 
53     public String getSubject() {
54         return subject;
55     }
56 
57     public void setSubject(String subject) {
58         this.subject = subject;
59     }
60 
61     public String getAppellation() {
62         return appellation;
63     }
64 
65     public void setAppellation(String appellation) {
66         this.appellation = appellation;
67     }
68 
69     public String getContext() {
70         return context;
71     }
72 
73     public void setContext(String context) {
74         this.context = context;
75     }
76 
77     public String getTail() {
78         return tail;
79     }
80 
81     public void setTail(String tail) {
82         this.tail = tail;
83     }
84 }
技術分享圖片

  Mail 就是一個業務對象,我們再來看業務場景類是怎麽調用的:

技術分享圖片
 1 package com.pattern.prototype;
 2 
 3 import java.util.Random;
 4 
 5 public class Client {
 6     // 發送賬單的數量,這個是值是從數據庫中獲得
 7     private static int MAX_COUNT = 6;
 8     
 9     public static void main(String[] args) {
10         // 模擬發送郵件
11         int i = 0;
12         // 把模版定義出來,這個是從數據庫中獲得
13         Mail mail = new Mail(new AdvTemplate());
14         mail.setTail("XX銀行版權所有");
15         while(i < MAX_COUNT){
16             // 以下是每封郵件不同的地方
17             mail.setAppellation(getRandString(5) + " 先生(女士)");
18             mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
19             // 然後發送郵件
20             sendMail(mail);
21             i ++;
22         }
23     }
24     
25     // 發送郵件
26     public static void sendMail(Mail mail){
27         System.out.println("標題:" + mail.getSubject() + "\t收件人:"
28                 + mail.getReceiver() + "\t...發送成功");
29     }
30     
31     // 獲得指定長度的隨機字符串
32     public static String getRandString(int maxLength){
33         String source = "abcdefghijklmnopqrskuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
34         StringBuffer sb = new StringBuffer();
35         Random rand = new Random();
36         for(int i=0;i<maxLength;i++){
37             sb.append(source.charAt(rand.nextInt(source.length())));
38         }
39         return sb.toString();
40     }
41 }
技術分享圖片

  由於是隨機數,每次運行都由所差異,不管怎麽樣,我們這個電子賬單發送程序時寫出來了,也能發送出來了,我們再來仔細的想想,這個程序是否有問題?你看,你這是一個線程在運行,也就是你發送是單線程的, 那按照一封郵件發出去需要 0.02 秒(夠小了,你還要到數據庫中取數據呢), 600 萬封郵件需要…我算算(掰指頭計算中…),恩,是 33 個小時,也就是一個整天都發送不完畢,今天發送不完畢,明天的賬單又產生了,積累積累,激起甲方人員一堆抱怨,那怎麽辦?

  好辦,把 sendMail 修改為多線程,但是你只把 sendMail 修改為多線程還是有問題的呀,你看哦,產生第一封郵件對象,放到線程 1 中運行,還沒有發送出去;線程 2 呢也也啟動了,直接就把郵件對象 mail的收件人地址和稱謂修改掉了,線程不安全了,好了,說到這裏,你會說這有 N 多種解決辦法,我們不多說,我們今天就說一種,使用原型模式來解決這個問題,使用對象的拷貝功能來解決這個問題,類圖稍作修改,如下圖:

技術分享圖片

  增加了一個 Cloneable 接口, Mail 實現了這個接口, 在 Mail 類中重寫了 clone()方法, 我們來看 Mail類的改變:

技術分享圖片
 1 package com.pattern.prototype;
 2 
 3 public class Mail implements Cloneable {
 4     // 收件人
 5     private String receiver;
 6     // 郵件名稱
 7     private String subject;
 8     // 稱謂
 9     private String appellation;
10     // 郵件內容
11     private String context;
12     // 郵件的尾部,一般都是加上"XXX版權所有"等信息
13     private String tail;
14 
15     // 構造函數
16     public Mail(AdvTemplate advTemplate) {
17         this.context = advTemplate.getAdvContext();
18         this.subject = advTemplate.getAdvSubject();
19     }
20 
21     public String getReceiver() {
22         return receiver;
23     }
24 
25     public void setReceiver(String receiver) {
26         this.receiver = receiver;
27     }
28 
29     public String getSubject() {
30         return subject;
31     }
32 
33     public void setSubject(String subject) {
34         this.subject = subject;
35     }
36 
37     public String getAppellation() {
38         return appellation;
39     }
40 
41     public void setAppellation(String appellation) {
42         this.appellation = appellation;
43     }
44 
45     public String getContext() {
46         return context;
47     }
48 
49     public void setContext(String context) {
50         this.context = context;
51     }
52 
53     public String getTail() {
54         return tail;
55     }
56 
57     public void setTail(String tail) {
58         this.tail = tail;
59     }
60     
61     public Mail clone(){
62         Mail mail = null;
63         try {
64             mail = (Mail) super.clone();
65         } catch (Exception e) {
66             e.printStackTrace();
67         }
68         return mail;
69     }
70     
71 }
技術分享圖片

  就做了一點修改,大家可能看著這個類有點奇怪,先保留你的好奇,我們繼續講下去,我會給你解答的,看 Client 類的改變:

技術分享圖片
 1 package com.pattern.prototype;
 2 
 3 import java.util.Random;
 4 
 5 public class Client {
 6     // 發送賬單的數量,這個是值是從數據庫中獲得
 7     private static int MAX_COUNT = 6;
 8     
 9     public static void main(String[] args) {
10         // 模擬發送郵件
11         int i = 0;
12         // 把模版定義出來,這個是從數據庫中獲得
13         Mail mail = new Mail(new AdvTemplate());
14         mail.setTail("XX銀行版權所有");
15         while(i < MAX_COUNT){
16             // 以下是每封郵件不同的地方
17             Mail cloneMail = mail.clone();
18             cloneMail.setAppellation(getRandString(5) + "先生(女士)");
19             cloneMail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
20             // 然後發送郵件
21             sendMail(mail);
22             i ++;
23         }
24     }
25     
26     // 發送郵件
27     public static void sendMail(Mail mail){
28         System.out.println("標題:" + mail.getSubject() + "\t收件人:"
29                 + mail.getReceiver() + "\t...發送成功");
30     }
31     
32     // 獲得指定長度的隨機字符串
33     public static String getRandString(int maxLength){
34         String source = "abcdefghijklmnopqrskuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
35         StringBuffer sb = new StringBuffer();
36         Random rand = new Random();
37         for(int i=0;i<maxLength;i++){
38             sb.append(source.charAt(rand.nextInt(source.length())));
39         }
40         return sb.toString();
41     }
42 }
技術分享圖片

  執行結果不變,一樣完成了電子廣告信的發送功能,而且 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 是不會被執行的,我們來寫一小段程序來說明這個問題:

技術分享圖片
 1 package com.pattern.prototype.clone_advance0;
 2 
 3 public class Thing implements Cloneable {
 4     
 5     public Thing(){
 6         System.out.println("構造函數被執行了...");
 7     }
 8     
 9     public Thing clone(){
10         Thing thing = null;
11         try {
12             thing = (Thing) super.clone();
13         } catch (CloneNotSupportedException e) {
14             e.printStackTrace();
15         }
16         return thing;
17     }
18     
19 }
20 
21 // 然後我們再來寫一個 Client 類,進行對象的拷貝:
22 
23 package com.pattern.prototype.clone_advance0;
24 
25 public class Client {
26     
27     public static void main(String[] args) {
28         // 產生一個對象
29         Thing thing = new Thing();
30         
31         // 拷貝一個對象
32         Thing cloneThing = thing.clone();
33         System.out.println(cloneThing);
34     }
35     
36 }
技術分享圖片

  對象拷貝時確實構造函數沒有被執行,這個從原理來講也是可以講得通的,Object 類的 clone 方法的原理是從內存中(具體的說就是堆內存)以二進制流的方式進行拷貝,重新分配一個內存塊,那構造函數沒有被執行也是非常正常的了。

  淺拷貝和深拷貝問題。 在解釋什麽是淺拷貝什麽是拷貝前,我們先來看個例子:

技術分享圖片
 1 package com.pattern.prototype.clone_advance1;
 2 
 3 import java.util.ArrayList;
 4 
 5 public class Thing implements Cloneable {
 6     
 7     // 定義一個私有變量
 8     private ArrayList<String> arrayList = new ArrayList<String>();
 9     
10     public Thing clone(){
11         Thing thing = null;
12         try {
13             thing = (Thing) super.clone();
14         } catch (CloneNotSupportedException e) {
15             e.printStackTrace();
16         }
17         return thing;
18     }
19     
20     // 設置ArrayList的值
21     public void setValue(String value){
22         this.arrayList.add(value);
23     }
24     
25     // 取得arrayList的值
26     public ArrayList<String> getValue(){
27         return this.arrayList;
28     }
29     
30 }
技術分享圖片

  在 Thing 類中增加一個私有變量 arrayList,類型為 ArrayList,然後通過 setValue 和 getValue 分別進行設置和取值,我們來看場景類:

技術分享圖片
 1 package com.pattern.prototype.clone_advance1;
 2 
 3 public class Client {
 4     
 5     public static void main(String[] args) {
 6         // 產生一個對象
 7         Thing thing = new Thing();
 8         // 設置一個值
 9         thing.setValue("張三");
10         
11         // 拷貝一個對象
12         Thing cloneThing = thing.clone();
13         cloneThing.setValue("李四");
14         
15         System.out.println(thing.getValue());
16     }
17     
18 }
技術分享圖片

  大家猜想一下運行結果應該是什麽?是就一個“張三”嗎?

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

  淺拷貝是有風險的,那怎麽才能深入的拷貝呢?我們修改一下我們的程序:

技術分享圖片
 1 package com.pattern.prototype.clone_advance3;
 2 
 3 import java.util.ArrayList;
 4 
 5 public class Thing implements Cloneable {
 6     
 7     // 定義一個私有變量
 8     private ArrayList<String> arrayList = new ArrayList<String>();
 9     
10     @SuppressWarnings("unchecked")
11     public Thing clone(){
12         Thing thing = null;
13         try {
14             thing = (Thing) super.clone();
15             thing.arrayList = (ArrayList<String>) this.arrayList.clone();
16         } catch (CloneNotSupportedException e) {
17             e.printStackTrace();
18         }
19         return thing;
20     }
21     
22 }
技術分享圖片

  僅僅增加了一行部分,Client 類沒有任何改變。

  這個實現了完全的拷貝,兩個對象之間沒有任何的瓜葛了,你修改你的,我修改我的,不相互影響,這種拷貝就叫做深拷貝,深拷貝還有一種實現方式就是通過自己寫二進制流來操作對象,然後實現對象的深拷貝,這個大家有時間自己實現一下。

  深拷貝和淺拷貝建議不要混合使用,一個類中某些引用使用深拷貝某些引用使用淺拷貝,這是一種非常差的設計,特別是是在涉及到類的繼承,父類有幾個引用的情況就非常的復雜,建議的方案深拷貝和淺拷貝分開實現。

  Clone 與 final 兩對冤家。 對象的 clone 與對象內的 final 屬性是由沖突的, 我們舉例來說明這個問題:

技術分享圖片
 1 package com.pattern.prototype.clone_advance3;
 2 
 3 import java.util.ArrayList;
 4 
 5 public class Thing implements Cloneable {
 6     
 7     // 定義一個私有變量
 8     private final ArrayList<String> arrayList = new ArrayList<String>();
 9     
10     @SuppressWarnings("unchecked")
11     public Thing clone(){
12         Thing thing = null;
13         try {
14             thing = (Thing) super.clone();
15             thing.arrayList = (ArrayList<String>) this.arrayList.clone();
16         } catch (CloneNotSupportedException e) {
17             e.printStackTrace();
18         }
19         return thing;
20     }
21     
22 }
技術分享圖片

  上面的代碼僅僅增加了一個 final 關鍵字,然後編譯器就報錯誤,正常呀,final 類型你還想重寫設值呀!完蛋了,你要實現深拷貝的夢想在 final關鍵字的威脅下破滅了,路總是有的,我們來想想怎麽修改這個方法:刪除掉 final 關鍵字,這是最便捷最安全最快速的方式,你要使用 clone 方法就在類的成員變量上不要增加 final 關鍵字。

  原型模式適合在什麽場景使用?一是類初始化需要消化非常多的資源,這個資源包括數據、硬件資源等;二是通過 new 產生一個對象需要非常繁瑣的數據準備或訪問權限,則可以使用原型模式;三是一個對象需要提供給其他對象訪問,而且各個調用者可能都需要修改其值時,可以考慮使用原型模式拷貝多個對象供調用者使用。在實際項目中,原型模式很少單獨出現,一般是和工廠方法模式一起出現,通過 clone的方法創建一個對象,然後由工廠方法提供給調用者。

  原型模式先產生出一個包含大量共有信息的類,然後可以拷貝出副本,修正細節信息,建立了一個完整的個性對象。不知道大家有沒有看過施瓦辛格演的《第六日》這個電影,電影的主線也就是一個人被復制,然後正本和副本對掐,我們今天講的原型模式也就是由一個正本可以創建多個副本的概念,可以這樣理解一個對象的產生可以不由零開始,直接從一個已經具備一定雛形的對象克隆,然後再修改為一個生產需要的對象。也就是說,產生一個人,可以不從 1 歲長到 2 歲,再 3 歲…,也可以直接找一個人,從其身上獲得 DNS,然後克隆一個,直接修改一下就是 3 歲的了!,我們講的原型模式也就是這樣的功能,是緊跟時代潮流的。

原文地址:https://www.cnblogs.com/initial-road/p/prototype_pattern.html

(轉載)24種設計模式--原型模式【Prototype Pattern】