設(shè)計模式之原型模式

原型模式的介紹

原型模式是一個創(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類圖如下:

原型模式1.png

  • 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 輸出的是一樣的,即document2document 的一份拷貝,他們的內(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)造了一份,而是副本文檔的字段引用原始文檔的字段,如圖:

淺拷貝.png

我們知道 A 引用 B 就是說兩個對象指向同一個地址,當修改 AB 也會改變,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中了,這是怎么回事呢?這是因為上文中只是簡單地進行了淺拷貝,引用類型的新對象 document2mImages 只是單純地指向了this.mImages引用,并沒有重新構(gòu)造一個mImages對象,然后將原始文檔中的圖片添加到新的mImages 對象中,這樣就導致document2 中的mImages 與原始文檔中的是同一個對象,因此,修改了其中一個文檔也會受影響。document2mImages 添加了新的圖片,實際上也就是往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

設(shè)計模式Demo

參考

《Android源碼設(shè)計模式》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容