我們今天來考慮一下給用戶郵箱發(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、long、String(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