原型模式的介紹
原型模式是一個創(chuàng)建型的模式,原型二字表明了該模式應該有一個樣板實例,用戶從這個樣板對象中復制出一個內(nèi)部屬性一直的對象,這個過程也就是我們俗稱的“克隆”。被復制的實例就是我們所稱的“原型”,這個原型是可定制的。原型模式多用于創(chuàng)建復雜的或者構(gòu)造耗時的實例,因為這種情況下,復制一個已經(jīng)存在的實例可使程序運行更高效。
原型模式的定義
用原型實例指定創(chuàng)建對象的種類,并通過復制這些原型創(chuàng)建新的對象。
原型模式的使用場景
- 類初始化需要消耗非常多的資源,這個資源包括數(shù)據(jù)、硬件、資源等,使用原型復制避免這些消耗
- 通過new 產(chǎn)生一個對象需要非常繁瑣的數(shù)據(jù)準備或訪問權(quán)限,這時可以使用原型模式
- 一個對象需要提供給其他對象訪問,而且各個調(diào)用者可能需要修改其他值,可以考慮使用原型模式復制多個對象供調(diào)用者使用,即保護性拷貝。
需要注意的是,通過實現(xiàn)Cloneable接口的原型模式在調(diào)用clone函數(shù)構(gòu)造實例時并不一定比通過new操作快,只有當通過new構(gòu)造對象較為耗時或者說成本較高時,通過clone方法才能夠獲得效率上的提升。因此,在使用Cloneable 是需要考慮構(gòu)建對象的成本已經(jīng)做一些效率上的測試,當然,實現(xiàn)原型模式也不一定非要實現(xiàn)Cloneable接口,也有其他的方式。
原型模式的UML類圖
UML類圖如下:

- Client :客戶端用戶。
-
ProtoType :抽象類或接口,聲明具備
clone能力。 - ContretePrototype :具體的原型類。
原型模式的簡單實現(xiàn)
首先創(chuàng)建一個文檔對象,即WordDocument,這個文檔中含有文字和圖片。用戶經(jīng)過長時間的內(nèi)容編輯后,打算對文檔做進一步的編輯,但是,這個編輯后的文檔是否被采用還不確定,因此,為了安全起見,用戶需要將當前文檔拷貝一份,然后在文檔副本上進行修改,如此,這個原始文檔就是我們上述所說的樣板實例,也就是將要被”克隆“的對象,我們稱為原型。
public class WordDocument implements Cloneable {
private String mText;
private ArrayList<String> mImages = new ArrayList<>();
public WordDocument() {
System.out.println("-------構(gòu)造函數(shù)-------");
}
public void setmText(String mText) {
this.mText = mText;
}
public void addImages(String mImages) {
this.mImages.add(mImages);
}
public void show(){
System.out.println("-------------Word Content Start-------------");
System.out.println("mText" + mText);
System.out.println("mImages" );
for (String s: mImages ) {
System.out.println("ImageName:"+s);
}
System.out.println("-------------Word Content end-------------\n");
}
@Override
protected WordDocument clone() {
try {
WordDocument doc = (WordDocument) super.clone();
doc.mText = this.mText;
doc.mImages = this.mImages;
return doc;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
//客戶端的使用
@Test
public void main() {
WordDocument document = new WordDocument();
document.setmText("這是一篇文章");
document.addImages("圖片1");
document.addImages("圖片2");
document.addImages("圖片3");
document.show();
//拷貝一份副本
WordDocument document2 = document.clone();
document2.show();
document2.setmText("這是修改過的文本2");
document2.show();
document.show();
}
}
通過WordDocument類模擬了Word文檔中的基本元素,即文字和圖片。WordDocument在該原型模式示例中扮演的角色為ConcreteProtoType,而Cloneable的角色為ProtoType。WordDocument中的clone方法用以實現(xiàn)對象克隆,注意,這個方法并不是Cloneable接口中的,而是Object中的方法。Cloneable也是一個標識接口,它表明這個類的對象是可拷貝的。而沒有實現(xiàn)Cloneable接口去調(diào)用了clone方法就會拋出異常。在這個實例中,通過實習Cloneable接口和重寫clone方法實現(xiàn)原型模式。
? 輸出結(jié)果:
-------構(gòu)造函數(shù)-------
-------------Word Content Start-------------
mText這是一篇文章
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
-------------Word Content end-------------
-------------Word Content Start-------------
mText這是一篇文章
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
-------------Word Content end-------------
-------------Word Content Start-------------
mText這是修改過的文本2
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
-------------Word Content end-------------
-------------Word Content Start-------------
mText這是一篇文章
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
-------------Word Content end-------------
結(jié)合代碼已經(jīng)運行結(jié)果,document2 是通過document .clone 創(chuàng)建的,并且document2第一次輸出的時候和document 輸出的是一樣的,即document2 是document 的一份拷貝,他們的內(nèi)容是一樣的,而document2 修改的文本內(nèi)容以后并不會影響document 的文本內(nèi)容,這就保證了document 的安全性。還需要注意的是,通過clone 拷貝對象時并不會執(zhí)行構(gòu)造函數(shù)!因此,如果在構(gòu)造函數(shù)中需要一些特殊的初始化操作的類型,在使用Cloneable 實現(xiàn)拷貝時,需要注意構(gòu)造函數(shù)不會執(zhí)行的問題。
深拷貝和淺拷貝
上述原型模式的實現(xiàn)實際上只是一個淺拷貝,也稱為影子拷貝。這份拷貝實際上并不是將原始文檔的所有字段都重新構(gòu)造了一份,而是副本文檔的字段引用原始文檔的字段,如圖:

我們知道 A 引用 B 就是說兩個對象指向同一個地址,當修改 A 時 B 也會改變,B修改時A 同樣也會改變。將測試方法修改如下:
@Test
public void main() {
WordDocument document = new WordDocument();
document.setmText("這是一篇文章");
document.addImages("圖片1");
document.addImages("圖片2");
document.addImages("圖片3");
document.show();
//拷貝一份副本
WordDocument document2 = document.clone();
document2.show();
document2.setmText("這是修改過的文本2");
document2.addImages("哈哈.png");
document2.show();
document.show();
}
輸出結(jié)果
-------構(gòu)造函數(shù)-------
-------------Word Content Start-------------
mText這是一篇文章
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
-------------Word Content end-------------
-------------Word Content Start-------------
mText這是一篇文章
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
-------------Word Content end-------------
-------------Word Content Start-------------
mText這是修改過的文本2
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
ImageName:哈哈.png
-------------Word Content end-------------
-------------Word Content Start-------------
mText這是一篇文章
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
ImageName:哈哈.png
-------------Word Content end-------------
我們發(fā)現(xiàn),最后兩個文檔信息輸出是一致的,我們在document2 添加了一張名為“哈哈.png”的照片,但是,同時也顯示在document中了,這是怎么回事呢?這是因為上文中只是簡單地進行了淺拷貝,引用類型的新對象 document2的mImages 只是單純地指向了this.mImages引用,并沒有重新構(gòu)造一個mImages對象,然后將原始文檔中的圖片添加到新的mImages 對象中,這樣就導致document2 中的mImages 與原始文檔中的是同一個對象,因此,修改了其中一個文檔也會受影響。document2 的mImages 添加了新的圖片,實際上也就是往document 里添加新的圖片,所以,document 里面也有“哈哈.png” 圖片文件,那么如何解決呢?答案就是采用深拷貝,記在拷貝對象時,對于引用類型的字段也要采用拷貝的形式,而不是單純引用的形式。clone方法修改如下:
protected WordDocument clone() {
try {
WordDocument doc = (WordDocument) super.clone();
doc.mText = this.mText;
//對mImaages 對象也調(diào)用clone函數(shù)進行拷貝
doc.mImages = ((ArrayList<String>) this.mImages.clone());
return doc;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
如上述代碼所示,將doc.mImages指向this.mImages的一份拷貝,而不是this.mImages本身,這樣在document2 添加圖片時并不會影響ducument ,運行結(jié)果如下:
-------構(gòu)造函數(shù)-------
-------------Word Content Start-------------
mText這是一篇文章
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
-------------Word Content end-------------
-------------Word Content Start-------------
mText這是一篇文章
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
-------------Word Content end-------------
-------------Word Content Start-------------
mText這是修改過的文本2
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
ImageName:哈哈.png
-------------Word Content end-------------
-------------Word Content Start-------------
mText這是一篇文章
mImages
ImageName:圖片1
ImageName:圖片2
ImageName:圖片3
-------------Word Content end-------------
原型模式是非常簡單的一個模式,它的核心問題就是對原始對象進行拷貝,在這個模式的使用過程中需要注意的一點就是:深淺拷貝的問題。在開發(fā)中為了減少錯誤,建議在使用該模式是盡量使用深拷貝,避免操作副本是影響原始對象的問題。
總結(jié)
原始模式本質(zhì)就是對象拷貝,與C++ 中的拷貝構(gòu)造函數(shù)有些類似,他們之間容易出現(xiàn)的問題也都是深、淺拷貝。使用原型模式可以解決構(gòu)建復雜對象的資源消耗問題,能夠在某些場景下提升創(chuàng)建對象的效率。還有一個重要的用途就是保護性拷貝,也就是某個對象對外可能是只讀的。為了防止外部對這個只讀對象修改,通??梢酝ㄟ^返回一個對象拷貝的形式實現(xiàn)只讀的限制。
優(yōu)點
原型模式是在內(nèi)存中二進制流的拷貝,要比直接new 一個對象性能好很多,特別是在一個循環(huán)體內(nèi)產(chǎn)生大量的對象時,原型模式可以更好地體現(xiàn)其優(yōu)點。
缺點
這既是優(yōu)點也是缺點,直接在內(nèi)存中拷貝,構(gòu)造函數(shù)是不會執(zhí)行的。開發(fā)中應該注意這個潛在的問題。
Demo
參考
《Android源碼設(shè)計模式》