1.原型模式介紹
原型模式是一個創建型的模式。原型二字表明瞭該模式應該有一個樣板實例,用戶從這個樣板對象中複製出一個內部屬性一致的對象,這個過程也就是我們俗稱的“克隆”。被複制的實例就是我們所稱的“原型”,這個原型是可定製的。原型模式多用於創建複雜的或者者構造耗時的實例,因爲這種情況下,複製一個已經存在的實例可使程序運行更高效。
2,原型模式的定義
用原型實例制定創建對象的種類,並通過拷貝這些原型創建新的對象。
3.原型模式的使用場景
- 類初始化需要消化非常多的資源,這個資源包括數據、硬件資源等,通過原型拷貝避免這些消耗。
- 通過new產生一個對象需要非常繁瑣的數據準備或訪問權限,這時可以使用原型模式。
- 一個對象需要提供給其它對象訪問,而且各個調用者可能都需要修改其值時,可以考慮使用原型模式拷貝多個對象供調用者使用,即保護性拷貝。
需要注意的是,通過實現Cloneable接口的原型模式在調用clone函數構造實例時並不一定比通過new操作快,只有當通過new構造對象較爲耗時或者說成本較高時,通過clone方法才能夠獲得效率上的提升。因此,在使用Cloneable時需要考慮構建對象的成本以及做一些效率上的測試。
4,原型模式的UML類圖
角色介紹:
- Client: 客戶端用戶;
- Prototype:抽象類或者接口,聲明具備clone能力;
- ConcretePrototype:具體的原型類。
5.原型模式的簡單實現
下面以簡單文檔拷貝爲例來演示一下簡單的原型模式,我們在這個例子中首先創建了一個文檔對象,即WordDocument,這個文檔中含有文字和圖片。用戶經過了長時間的內容編輯後,打算對該文檔做進一步編輯,但是,這個編輯後的文檔是否會被採用還不確定,因此,爲了安全起見,用戶需要將當前文檔拷貝一份,然後再在文檔副本上進行修改,這樣,這個原始文檔就是我們上述所說的樣板實例,也就是將要被“克隆”的對象,我們稱爲原型:
/**
*文檔類型,扮演的是ConcretePrototype角色,而cloneable是代表*prototype角色
*/
public class WordDocument implements Cloneable {
// 文本
private String mText;
// 圖片名稱列表
private ArrayList<String> mImages = new ArrayList<String>();
public WordDocument() {
System.out.println("----------WordDocument構造函數-----------");
}
@Override
protected WordDocument clone() {
try {
WordDocument doc = (WordDocument) super.clone();
doc.mText = this.mText;
doc.mImages = this.mImages;
return doc;
} catch (Exception e) {
}
return null;
}
public String getText() {
return mText;
}
public void setText(String mText){
this.mText=mText;
}
public List<String> getImages(){
return mImages;
}
public void addImage(String img){
this.mImages.add(img);
}
/**
* 打印文檔內容
*/
public void showDocument(){
System.out.println("----------Word Content Start----------");
System.out.println("Text:"+mText);
System.out.println("Image List:");
for (String imgName:mImages) {
System.out.println("image name:"+imgName);
}
System.out.println("----------Word Content End------------");
}
}
通過WordDocument類模擬Word文檔中的基本元素,即文字和圖片。WordDocument在該原型模式示例中扮演的角色爲ConcretePrototype,而Cloneable的角色則爲Prototype。WordDocument中的clone方法用以實現對象克隆。注意,這個方法並不是Cloneable接口中的,而是Object中的方法。Cloneable也是一個標識接口,它表示這個類的對象是拷貝的。如果沒有實現Cloneable接口卻調用了clone()函數將拋出異常。在這個示例中,我們通過實現Cloneable接口和覆寫clone方法實現原型模式。
Client類:
public class Client {
public static void main(String[] args) {
// 1.構建文檔對象
WordDocument originDoc = new WordDocument();
// 2.編輯文檔,添加圖片等
originDoc.setText("這是一篇文檔");
originDoc.addImage("圖片1");
originDoc.addImage("圖片2");
originDoc.addImage("圖片3");
originDoc.showDocument();
// 以原始文檔爲原型,拷貝一份副本
WordDocument doc2 = originDoc.clone();
doc2.showDocument();
// 修改文檔副本,不會影響原始文檔
doc2.setText("這是修改過的Doc2文本");
doc2.showDocument();
originDoc.showDocument();
}
}
輸出:
----------WordDocument構造函數-----------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
----------Word Content End------------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
----------Word Content End------------
----------Word Content Start----------
Text:這是修改過的Doc2文本
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
----------Word Content End------------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
----------Word Content End------------
從輸出可看出,doc2是通過originDoc.clone()創建的,並且doc2第一次輸出的時候和originDoc輸出是一樣,即doc2是originDoc的一份拷貝,它們的內容是一樣的,而doc2修改了文本內容以後並不影響originDoc的文本內容,這就保證了originDoc的安全性。還需要注意的是,通過clone拷貝對象時並不會執行構造函數。因此,如果在構造函數中需要一些特殊的初始化操作的類型,在使用Cloneable實現拷貝時,需要注意構造函數不會執行的問題。
6.淺拷貝和深拷貝
上述原型模式的實現實際上只是一個淺拷貝,也稱影子拷貝,這份拷貝實際上並不是將原始文檔的所有字段都重新構造一份,而是 副本文檔的字段引用原始文檔的字段,如圖:
我們知道A引用B就是說兩個對象指向同一個地址,當修改A時B也會改變,B修改時A同樣會改變。我們直接看下面的例子,將main函數的內容修改爲如下:
public class Client {
public static void main(String[] args) {
// 1.構建文檔對象
WordDocument originDoc = new WordDocument();
// 2.編輯文檔,添加圖片等
originDoc.setText("這是一篇文檔");
originDoc.addImage("圖片1");
originDoc.addImage("圖片2");
originDoc.addImage("圖片3");
originDoc.showDocument();
// 以原始文檔爲原型,拷貝一份副本
WordDocument doc2 = originDoc.clone();
doc2.showDocument();
// 修改文檔副本,不會影響原始文檔
// doc2.setText("這是修改過的Doc2文本");
// doc2.showDocument();
doc2.addImage("哈哈哈.jpg");
doc2.showDocument();
originDoc.showDocument();
}
}
輸出結果:
----------WordDocument構造函數-----------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
----------Word Content End------------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
----------Word Content End------------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
image name:哈哈哈.jpg
----------Word Content End------------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
image name:哈哈哈.jpg
----------Word Content End------------
最後兩份文檔信息輸出是一致的。我們在doc2添加了一張名爲“哈哈哈.jpg”的圖片,但是,同時也顯示在originDoc中了,這是因爲上文中WordDocument的clone方法中只是簡單地進行淺拷貝,引用類型的新對象doc2的mImages只是單純地指向了this.mIages引用,並沒重新構造一個mImages對象,然後將原文檔中的圖片添加到新的mImages對象中,這樣就導致doc2中的mImages與原始文檔中的是同一個對象,因此,修改了其中一個文檔中的圖片,另一個文檔也會受影響。doc2的mImages添加了新的圖片,實際上也就是往originDoc裏添加了新圖片,所以,originDoc裏面也有“哈哈哈.jpg”圖片文件。那如何解決這個問題呢?答案就是採用深拷貝,即 在拷貝對象時,對於引用型的字段也要採用拷貝的形式,而不是單純的形式,而不是單純引用的形式。 clone方法修改如下:
@Override
protected WordDocument clone() {
try {
WordDocument doc = (WordDocument) super.clone();
doc.mText = this.mText;
// 淺拷貝
// doc.mImages = this.mImages;
// 深拷貝
doc.mImages = (ArrayList<String>) this.mImages.clone();
return doc;
} catch (Exception e) {
}
return null;
}
如上述代碼所示,將doc.mImages指向this.mImages的一份拷貝,而不是this.mImages本身, 這樣在doc2添加圖片時並不會影響originDoc,運行效果如下:
----------WordDocument構造函數-----------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
----------Word Content End------------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
----------Word Content End------------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
image name:哈哈哈.jpg
----------Word Content End------------
----------Word Content Start----------
Text:這是一篇文檔
Image List:
image name:圖片1
image name:圖片2
image name:圖片3
----------Word Content End------------
原型模式是非常簡單的模式,它的核心問題就是對原始對象進行拷貝,在這個模式的使用過程中需要注意的一點就是:深、淺拷貝的問題。爲了減少出錯,建議大家在使用該模式時使用深拷貝,避免操作副本時影響原始對象的問題。
————摘自《Android 源碼設計模式解析與實戰 第四章》