文章目錄
原型模式是一個比較簡單,但應用頻率比較高的設計模式。
Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.(用原型執行個體指定建立對象的種類,并且通過拷貝這些原型建立新的對 象。) |
原型模式的通用類圖如下:
圖13-1:原型模式通用類圖
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SNjFjN2IGZ1UTMiNjYjNjY5gTN0QDZ0IzN0QzYlZzNi9CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.png)
原型模式的核心是一個clone方法,通過該方法進行對象的拷貝,Java 提供了一個Cloneable接口來标示這個對象是可拷貝的,Cloneable接口的作用是标記,在JVM中具有這個标記的對象才有可能被拷貝。那怎麼才能從“有可能被拷貝”轉換為“可以被拷貝”呢?方法是覆寫 clone()方法:
@Override public Mail clone(){}
在clone()方法上增加了一個注解@Override——因為覆寫了Object類的clone方法。
在Java中原型模式非常簡單,通用源碼如下:
public class PrototypeClass implements Cloneable{
//覆寫父類Object方法
@Override public PrototypeClass clone(){
PrototypeClass prototypeClass = null;
try {
prototypeClass = (PrototypeClass)super.clone();
} catch (CloneNotSupportedException e) {
//異常處理
}
return prototypeClass;
}
}
現在有這樣的業務場景:
每到月初的時候銀行會給信用卡使用者以郵件的方式發送一份電子賬單,包含使用者的消費情況、積分等,當然了,還有比較讨厭的廣告信。出于個性化服務和投遞成功率的考慮,廣告信也作為電子賬單系統的一個子功能。
這個功能大概這麼實作:指定一個模闆,從資料庫中把客戶的資訊一個一個地取出,放到模闆中生成一份完整的郵件, 然後由發送機進行發送處理。
結合上面的實作思路,相應的類圖如下:
圖13-2:發送電子賬單類圖
AdvTemplate是廣告信的模闆,一般都是從資料庫取出,生成一個BO或者是 DTO,這裡使用一個靜态的值來作代表;Mail是郵件類,發送機發送的就是這個類。
- AdvTemplate類:
/**
* @author 三分惡
* @date 2020年5月14日
* @description 廣告模闆類
*/
public class AdvTemplate {
//廣告信名稱
private String advSubject ="XX銀行國慶信用卡抽獎活動";
//廣告信内容
private String advContext = "國慶抽獎活動通知:隻要刷卡就送你一百萬!...";
public String getAdvSubject() {
return advSubject;
}
public String getAdvContext() {
return advContext;
}
}
- Mail類:
/**
* @author 三分惡
* @date 2020年5月14日
* @description 郵件類
*/
public class Mail {
//收件人
private String receiver;
//郵件名稱
private String subject;
//稱謂
private String appellation;
//郵件内容
private String contxt;
//郵件的尾部,一般都是加上"XXX版權所有"等資訊
private String tail;
//構造方法
public Mail(AdvTemplate advTemplate) {
this.contxt = advTemplate.getAdvContext();
this.subject = advTemplate.getAdvSubject();
}
//省略getter、setter方法
}
- Client場景類:
/**
* @author 三分惡
* @date 2020年5月14日
* @description 場景類
*/
public class Client {
// 發送賬單的數量
private static int MAX_COUNT = 6;
public static void main(String[] args) {
// 模拟發送郵件
int i = 0;
// 把模闆定義出來
Mail mail = new Mail(new AdvTemplate());
mail.setTail("XX銀行版權所有");
while (i < MAX_COUNT) {
// 以下是每封郵件不同的地方
mail.setAppellation(getRandString(5) + " 先生(女士)");
mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
// 然後發送郵件
sendMail(mail);
i++;
}
}
// 發送郵件
public static void sendMail(Mail mail) {
System.out.println("标題:" + mail.getSubject() + "\t收件人: " + mail.getReceiver() + "\t...發送成功!");
}
// 獲得指定長度的随機字元串
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();
}
}
運作結果:
OK,發送廣告信的功能到此實作了。
但是存在一個問題,這個程式時單線程的,按照一封郵件發出去需要0.02秒,600萬封郵件需要33個小時,也就是一個整天都發送不完,今天的沒發送完,明天的賬單又産生了。
如果用多線程的方式呢?那麼線程安全的問題又來了。産生第一封郵件對象,放到線程1中運作,還沒有發送出去;線程2也啟動了,直接就把郵件對 象mail的收件人位址和稱謂修改了。
解決的辦法有很多,其中一種就是通過原型模式。
類圖稍作修改:
圖13-3:發送電子賬單類圖
- Mail類實作Cloneable接口,覆寫clone()方法:
/**
* @author 三分惡
* @date 2020年5月14日
* @description 郵件類
*/
public class Mail implements Cloneable{
//收件人
private String receiver;
//郵件名稱
private String subject;
//稱謂
private String appellation;
//郵件内容
private String contxt;
//郵件的尾部,一般都是加上"XXX版權所有"等資訊
private String tail;
//構造方法
public Mail(AdvTemplate advTemplate) {
this.contxt = advTemplate.getAdvContext();
this.subject = advTemplate.getAdvSubject();
}
/**
* 覆寫clone方法
*/
@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方法
}
- Client場景類的修改:
public static void main(String[] args) {
// 模拟發送郵件
int i = 0;
// 把模闆定義出來
Mail mail = new Mail(new AdvTemplate());
mail.setTail("XX銀行版權所有");
while (i < MAX_COUNT) {
// 以下是每封郵件不同的地方
//這裡使用clone方法clone對象
Mail cloneMail = mail.clone();
//使用clone的對象
cloneMail.setAppellation(getRandString(5) + " 先生(女士)");
cloneMail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
// 然後發送郵件
sendMail(mail);
i++;
}
}
在設定郵件不同屬性的地方通過clone的方式産生一個新的對象,然後再修改細節的資料,如設定稱謂、設定收件人位址,這樣即使是多線程也不受影響。
原型模式的主要優點如下:
- 當建立新的對象執行個體較為複雜時,使用原型模式可以簡化對象的建立過程,通過複制一個已有執行個體可以提高新執行個體的建立效率。
- 原型模式提供了簡化的建立結構,工廠方法模式常常需要有一個與産品類等級結構相同的 工廠等級結構,而原型模式就不需要這樣,原型模式中産品的複制是通過封裝在原型類中的克隆方法實作的,無須專門的工廠類來建立産品。
- 可以使用深克隆的方式儲存對象的狀态,使用原型模式将對象複制一份并将其狀态儲存起 來,以便在需要的時候使用(如恢複到某一曆史狀态),可輔助實作撤銷操作。
原型模式的主要缺點如下:
- 需要為每一個類配備一個克隆方法,而且該克隆方法位于一個類的内部,當對已有的類進 行改造時,需要修改源代碼,違背了“開閉原則”。
- 在實作深克隆時需要編寫較為複雜的代碼,而且當對象之間存在多重的嵌套引用時,為了 實作深克隆,每一層對象對應的類都必須支援深克隆,實作起來可能會比較麻煩。
在Java語言中,資料類型分為值類型(基本資料類型)和引用類型,值類型包括 int、double、byte、boolean、char等簡單資料類型,引用類型包括類、接口、數組等複雜類 型。淺克隆和深克隆的主要差別在于是否支援引用類型的成員變量的複制。
在淺克隆中,如果原型對象的成員變量是值類型,将複制一份給克隆對象;如果原型對象的 成員變量是引用類型,則将引用對象的位址複制一份給克隆對象,也就是說原型對象和克隆 對象的成員變量指向相同的記憶體位址。簡單來說,在淺克隆中,當對象被複制時隻複制它本 身和其中包含的值類型的成員變量,而引用類型的成員對象并沒有複制。
來看一個執行個體:
- 在Thing類中定義一個私有變量arrayLis,類型為ArrayList,然後通過setValue和getValue 分别進行設定和取值
/**
* @author 三分惡
* @date 2020年5月14日
* @description
*/
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;
}
//設定HashMap的值
public void setValue(String value){
this.arrayList.add(value); }
// 取得arrayList的值
public ArrayList<String> getValue() {
return this.arrayList;
}
}
- 在場景類中克隆
/**
* @author 三分惡
* @date 2020年5月14日
* @description 淺克隆測試
*/
public class ShallowCloneClient {
public static void main(String[] args) {
// 産生一個對象
Thing thing = new Thing();
// 設定一個值
thing.setValue("二錘子");
// 拷貝一個對象
Thing cloneThing = thing.clone();
cloneThing.setValue("三棒子");
System.out.println(thing.getValue());
}
}
在這個例子中,對象thing和cloneThing的arrayList都指向了同一個位址,是以運作結果:
這裡就存在風險,兩個對象共享了一個私有變量,都能對這個變量進行修改。
使用原型模式時,引用的成員變量必須滿足兩個條件才不會被克隆:一是類的成 員變量,而不是方法内變量;二是必須是一個可變的引用對象,而不是一個原始類型或不可 變對象。
在深克隆中,無論原型對象的成員變量是值類型還是引用類型,都将複制一份給克隆對象, 深克隆将原型對象的所有引用對象也複制一份給克隆對象。簡單來說,在深克隆中,除了對象本身被複制外,對象所包含的所有成員變量也将複制。
将Thing類的clone方法進行修改:
@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;
}
改動很少,對私有的類變量進行獨立的拷貝,這樣一來,兩個對象的arrayList的指向位址就不一樣了。
還可以通過序列化的方式實作,序列化就是将對象寫到流的過程,寫到流中的對象是原有對象的一個拷貝,而原對象仍然存在于記憶體 中。通過序列化實作的拷貝不僅可以複制對象本身,而且可以複制其引用的成員對象,是以通過序列化将對象寫到一個流中,再從流裡将其讀出來,可以實作深克隆。
-
資源優化場景
類初始化需要消化非常多的資源,這個資源包括資料、硬體資源等。
-
性能和安全要求的場景
通過new産生一個對象需要非常繁瑣的資料準備或通路權限,則可以使用原型模式。
- 一個對象多個修改者的場景 一個對象需要提供給其他對象通路,而且各個調用者可能都需要修改其值時,可以考慮 使用原型模式拷貝多個對象供調用者使用。
在實際項目中,原型模式很少單獨出現,一般是和工廠方法模式一起出現,通過clone的 方法建立一個對象,然後由工廠方法提供給調用者。