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

在類圖中AdvTemplate 是廣告信的模板,一般都是從數(shù)據(jù)庫取出,生成一個BO 或者是DTO,我們這里使用一個靜態(tài)的值來做代表;Mail 類是一封郵件類,發(fā)送機(jī)發(fā)送的就是這個類,我們先來看看我們的程序:
package Test;public class AdvTemplate { // 廣告信名稱 PRivate String advSubject = "XX銀行國慶信用卡抽獎活動"; // 廣告信內(nèi)容 private String advContext = "國慶抽獎活動通知:只要刷卡就送你1百萬!...."; // 取得廣告信的名稱 public String getAdvSubject() { return this.advSubject; } // 取得廣告信的內(nèi)容 public String getAdvContext() { return this.advContext; }}我們再來看郵件類:package Test;public class Mail { // 收件人 private String receiver; // 郵件名稱 private String subject; // 稱謂 private String appellation; // 郵件內(nèi)容 private String contxt; // 郵件的尾部,一般都是加上“XXX版權(quán)所有”等信息 private String tail; // 構(gòu)造函數(shù) public Mail(AdvTemplate advTemplate) { this.contxt = advTemplate.getAdvContext(); this.subject = advTemplate.getAdvSubject(); } // 以下為getter/setter方法 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 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 就是一個業(yè)務(wù)對象,我們再來看業(yè)務(wù)場景類是怎么調(diào)用的:package Test;import java.util.Random;public class Client { // 發(fā)送賬單的數(shù)量,這個值是從數(shù)據(jù)庫中獲得 private static int MAX_COUNT = 6; public static void main(String[] args) { // 模擬發(fā)送郵件 int i = 0; // 把模板定義出來,這個是從數(shù)據(jù)庫中獲得 Mail mail = new Mail(new AdvTemplate()); mail.setTail("XX銀行版權(quán)所有"); while (i < MAX_COUNT) { // 以下是每封郵件不同的地方 mail.setAppellation(getRandString(5) + " 先生(女士)"); mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com"); // 然后發(fā)送郵件 sendMail(mail); i++; } } // 發(fā)送郵件 public static void sendMail(Mail mail) { System.out.println("標(biāo)題:" + mail.getSubject() + "/t收件人" + mail.getReceiver() + "/t....發(fā)送成功!"); } // 獲得指定長度的隨機(jī)字符串 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(); }}運(yùn)行結(jié)果如下:

由于是隨機(jī)數(shù),每次運(yùn)行都由所差異,不管怎么樣,我們這個電子賬單發(fā)送程序時寫出來了,也能發(fā)送出來了,我們再來仔細(xì)的想想,這個程序是否有問題?你看,你這是一個線程在運(yùn)行,也就是你發(fā)送是單線程的,那按照一封郵件發(fā)出去需要0.02 秒(夠小了,你還要到數(shù)據(jù)庫中取數(shù)據(jù)呢),600 萬封郵件需要…我算算(掰指頭計算中…),恩,是33 個小時,也就是一個整天都發(fā)送不完畢,今天發(fā)送不完畢,明天的賬單又產(chǎn)生了,積累積累,激起甲方人員一堆抱怨,那怎么辦?.
好辦,把sendMail 修改為多線程,但是你只把sendMail 修改為多線程還是有問題的呀,你看哦,產(chǎn)生第一封郵件對象,放到線程1 中運(yùn)行,還沒有發(fā)送出去;線程2 呢也也啟動了,直接就把郵件對象mail的收件人地址和稱謂修改掉了,線程不安全了,好了,說到這里,你會說這有N 多種解決辦法,我們不多說,我們今天就說一種,使用原型模式來解決這個問題,使用對象的拷貝功能來解決這個問題,類圖稍作修改,如下圖:

增加了一個Cloneable 接口, Mail 實現(xiàn)了這個接口,在Mail 類中重寫了clone()方法,我們來看Mail類的改變:
package Test;public class Mail implements Cloneable{ //收件人 private String receiver; //郵件名稱 private String subject; //稱謂 private String appellation; //郵件內(nèi)容 private String contxt; //郵件的尾部,一般都是加上“XXX版權(quán)所有”等信息 private String tail; //構(gòu)造函數(shù) public Mail(AdvTemplate advTemplate){ this.contxt = advTemplate.getAdvContext(); this.subject = advTemplate.getAdvSubject(); } @Override public Mail clone(){ Mail mail =null; try { mail = (Mail)super.clone(); } catch (CloneNotSupportedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return mail; } //以下為getter/setter方法 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 getContxt() { return contxt; } public void setContxt(String contxt) { this.contxt = contxt; } public String getTail() { return tail; } public void setTail(String tail) { this.tail = tail; }} 就黃色體部分做了修改,大家可能看著這個類有點(diǎn)奇怪,先保留你的好奇,我們繼續(xù)講下去,我會給你解答的,看Client 類的改變:package Test;import java.util.Random;public class Client { // 發(fā)送賬單的數(shù)量,這個值是從數(shù)據(jù)庫中獲得 private static int MAX_COUNT = 6; public static void main(String[] args) { // 模擬發(fā)送郵件 int i = 0; // 把模板定義出來,這個是從數(shù)據(jù)中獲得 Mail mail = new Mail(new AdvTemplate()); mail.setTail("XX銀行版權(quán)所有"); while (i < MAX_COUNT) { // 以下是每封郵件不同的地方 Mail cloneMail = mail.clone(); cloneMail.setAppellation(getRandString(5) + " 先生(女士)"); cloneMail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com"); // 然后發(fā)送郵件 sendMail(cloneMail); i++; } }} 運(yùn)行結(jié)果不變,一樣完成了電子廣告信的發(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()方法,是的,你沒有看錯是重寫clone()方法,看看我們上面Mail 類:
@Override public Mail clone(){ }
在 clone()方法上增加了一個注解@Override,沒有繼承一個類為什么可以重寫呢?在Java 中所有類的老祖宗是誰?對嘛,Object 類,每個類默認(rèn)都是繼承了這個類,所以這個用上重寫是非常正確的。原型模式雖然很簡單,但是在Java 中使用原型模式也就是clone 方法還是有一些注意事項的,我們通過幾個例子一個一個解說(如果你對Java 不是很感冒的話,可以跳開以下部分)。對象拷貝時,類的構(gòu)造函數(shù)是不會被執(zhí)行的。一個實現(xiàn)了Cloneable 并重寫了clone 方法的類A,有一個無參構(gòu)造或有參構(gòu)造B,通過new 關(guān)鍵字產(chǎn)生了一個對象S,再然后通過S.clone()方式產(chǎn)生了一個新的對象T,那么在對象拷貝時構(gòu)造函數(shù)B 是不會被執(zhí)行的,我們來寫一小段程序來說明這個問題:
package Test;public 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; }} 然后我們再來寫一個Client 類,進(jìn)行對象的拷貝:
package Test;public class Client1 { public static void main(String[] args) { // 產(chǎn)生一個對象 Thing thing = new Thing(); // 拷貝一個對象 Thing cloneThing = thing.clone(); }}運(yùn)行結(jié)果如下:
對象拷貝時確實構(gòu)造函數(shù)沒有被執(zhí)行,這個從原理來講也是可以講得通的,Object 類的clone 方法的原理是從內(nèi)存中(具體的說就是堆內(nèi)存)以二進(jìn)制流的方式進(jìn)行拷貝,重新分配一個內(nèi)存塊,那構(gòu)造函數(shù)沒有被執(zhí)行也是非常正常的了。
淺拷貝和深拷貝問題。在解釋什么是淺拷貝什么是拷貝前,我們先來看個例子:
package Test;public class Thing implements Cloneable { // 定義一個私有變量 private ArrayList<String> arrayList = new ArrayList<String>(); @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; }} 在Thing 類中增加一個私有變量arrayLis,類型為ArrayList,然后通過setValue 和getValue 分別進(jìn)行設(shè)置和取值,我們來看場景類:package Test;import java.util.Random;public class Client { public static void main(String[] args) { // 產(chǎn)生一個對象 Thing thing = new Thing(); // 設(shè)置一個值 thing.setValue("張三"); // 拷貝一個對象 Thing cloneThing = thing.clone(); cloneThing.setValue("李四"); System.out.println(thing.getValue()); }}大家猜想一下運(yùn)行結(jié)果應(yīng)該是什么?是就一個“張三”嗎?運(yùn)行結(jié)果如下:

怎么會有李四呢?是因為Java 做了一個偷懶的拷貝動作,Object 類提供的方法clone 只是拷貝本對象,其對象內(nèi)部的數(shù)組、引用對象等都不拷貝,還是指向原生對象的內(nèi)部元素地址,這種拷貝就叫做淺拷貝,確實是非常淺,兩個對象共享了一個私有變量,你改我改大家都能改,是一個種非常不安全的方式,在實際項目中使用還是比較少的。你可能會比較奇怪,為什么在Mail 那個類中就可以使用使用String 類型,而不會產(chǎn)生由淺拷貝帶來的問題呢?內(nèi)部的數(shù)組和引用對象才不拷貝,其他的原始類型比如int,long,String(Java 就希望你把String 認(rèn)為是基本類型,String 是沒有clone 方法的)等都會被拷貝的。
淺拷貝是有風(fēng)險的,那怎么才能深入的拷貝呢?我們修改一下我們的程序:
package Test; public class Thing implements Cloneable{ //定義一個私有變量 private ArrayList<String> arrayList = new ArrayList<String>(); @Override 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 類沒有任何改變,運(yùn)行結(jié)果如下:

這個實現(xiàn)了完全的拷貝,兩個對象之間沒有任何的瓜葛了,你修改你的,我修改我的,不相互影響,這種拷貝就叫做深拷貝,深拷貝還有一種實現(xiàn)方式就是通過自己寫二進(jìn)制流來操作對象,然后實現(xiàn)對象的深拷貝,這個大家有時間自己實現(xiàn)一下。
深拷貝和淺拷貝建議不要混合使用,一個類中某些引用使用深拷貝某些引用使用淺拷貝,這是一種非常差的設(shè)計,特別是是在涉及到類的繼承,父類有幾個引用的情況就非常的復(fù)雜,建議的方案深拷貝和淺拷貝分開實現(xiàn)。
Clone 與final 兩對冤家。對象的clone 與對象內(nèi)的final 屬性是由沖突的,我們舉例來說明這個問題:
package Test; public class Thing implements Cloneable { // 定義一個私有變量 private final ArrayList<String> arrayList = new ArrayList<String>(); @Override public Thing clone() { Thing thing = null; try { thing = (Thing) super.clone(); this.arrayList = (ArrayList<String>) this.arrayList.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return thing; } }} 黃色的部分僅僅增加了一個final 關(guān)鍵字,然后編譯器就報灰色的部分錯誤,正常呀,final 類型你還想重寫設(shè)值呀!完蛋了,你要實現(xiàn)深拷貝的夢想在final 關(guān)鍵字的威脅下破滅了,路總是有的,我們來想想怎么修改這個方法:刪除掉final 關(guān)鍵字,這是最便捷最安全最快速的方式,你要使用clone 方法就在類的成員變量上不要增加final 關(guān)鍵字。
原型模式適合在什么場景使用?一是類初始化需要消化非常多的資源,這個資源包括數(shù)據(jù)、硬件資源等;二是通過new 產(chǎn)生一個對象需要非常繁瑣的數(shù)據(jù)準(zhǔn)備或訪問權(quán)限,則可以使用原型模式;三是一個對象需要提供給其他對象訪問,而且各個調(diào)用者可能都需要修改其值時,可以考慮使用原型模式拷貝多個對象供調(diào)用者使用。在實際項目中,原型模式很少單獨(dú)出現(xiàn),一般是和工廠方法模式一起出現(xiàn),通過clone的方法創(chuàng)建一個對象,然后由工廠方法提供給調(diào)用者。
原型模式先產(chǎn)生出一個包含大量共有信息的類,然后可以拷貝出副本,修正細(xì)節(jié)信息,建立了一個完整的個性對象。不知道大家有沒有看過施瓦辛格演的《第六日》這個電影,電影的主線也就是一個人被復(fù)制,然后正本和副本對掐,我們今天講的原型模式也就是由一個正本可以創(chuàng)建多個副本的概念,可以這樣理解一個對象的產(chǎn)生可以不由零開始,直接從一個已經(jīng)具備一定雛形的對象克隆,然后再修改為一個生產(chǎn)需要的對象。也就是說,產(chǎn)生一個人,可以不從1 歲 長到2 歲,再3 歲…,也可以直接找一個人,從其身上獲得DNS,然后克隆一個,直接修改一下就是3 歲的了!,我們講的原型模式也就是這樣的功能,是緊跟時代潮流的。
新聞熱點(diǎn)
疑難解答