設計模式 ( 三 ) 原型模式

介紹

原型模式是一個建立型的模式。原型二字代表了該模式應該有一個模板實例,用戶從這個模板對象中複製出一個內部屬性一致而且內存地址不一樣的對象,這個過程也就是咱們俗稱的 "克隆" 。被複制的實例就是咱們所稱的 「原型」 ,這個原型是可定製的。原型模式多用於建立複雜的或者構造耗時的實例,由於這種狀況下,複製一個已經存在的實例可以使程序運行更高效。html

定義

用原型實例指定建立對象的種類,並經過複製這些原型建立新的對象。java

使用場景

  1. 類初始化須要消耗很是多的資源,這個資源包括數據、硬件資源等,經過原型複製避免這些消耗。
  2. 經過 new 產生一個對象須要很是繁瑣的數據準備和訪問權限,這時可使用原型模式。
  3. 一個對象須要提供給其它對象訪問,並且各個調用者可能都須要修改其值時,能夠考慮使用原型模式複製多個對象供調用者使用,既保護性拷貝。

UML 類圖

cGAVM.png

  • Client: 客戶端用戶。
  • Prototype: 抽象類或者接口,聲明具有 clone 能力。
  • ConcreatePrototype: 具體的原型類

原型模式的簡單實現

下面以簡單的文檔 copy 爲例來演示一下原型模式。android

需求:有一個文檔,文檔中包含了文字和圖片,用戶通過了長時間的內容編輯後,打算對該文檔作進一步的編輯,可是,這個編輯後的文檔是否會被採用還不肯定,所以,爲了安全起見,用戶須要將當前文檔 copy 一份,而後再在文檔副本上進行修改。git

/***這裏表明是具體原型類*/
public class WordDocument implements Cloneable {

    /** * 文本 */
    private String mTxt;

    /** * 圖片名列表 */
    private List<String> mImagePath = new ArrayList<>();


    public String getmTxt() {
        return mTxt;
    }

    public void setmTxt(String mTxt) {
        this.mTxt = mTxt;
    }

    public List<String> getImagePath() {
        return mImagePath;
    }

    public void addImagepath(String imagepath) {
        mImagePath.add(imagepath);
    }

    /** * 打印文檔內容 */
    public void println(){

        System.out.println("---------------- start ----------------");
        
        System.out.println("txt: " + mTxt);
        System.out.println("mImagePath: ");
        for (String path : mImagePath) {
            System.out.println("path: " + path);
        }

        System.out.println("----------------- end ----------------");
    }

    /** * 聲明具有 clone 能力 * @return clone 的對象 */
    @Override
    protected WordDocument clone() {
        try {
            WordDocument document = (WordDocument)super.clone();
            document.mTxt = this.mTxt;
            document.mImagePath = this.mImagePath;
            return document;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
}
複製代碼

Test:github

@Test
    public void test4(){
        //1. 構建文檔對象
        WordDocument wordDocument = new WordDocument();
        //2. 編輯文檔
        wordDocument.setmTxt("今天是一個好天氣");
        wordDocument.addImagepath("/sdcard/image.png");
        wordDocument.addImagepath("/sdcard/image2.png");
        wordDocument.addImagepath("/sdcard/image3.png");
        //打印文檔內容
        wordDocument.println();


        System.out.println("--------------------開始clone-----\n\n");

        //以原始文檔爲準,copy 副本
        WordDocument cloneDoc = wordDocument.clone();

        System.out.println(" 打印副本,看看數據 \n\n");
        //打印副本,看看數據
        cloneDoc.println();

        //在副本文檔上修改
        cloneDoc.setmTxt("副奔上修改文檔:老龍王哭了");
        System.out.println(" 打印修改後的副本 \n\n");
        //打印修改後的副本
        cloneDoc.println();
        System.out.println("----看會不會影響原始文檔-----\n\n");
        //看會不會影響原始文檔???????
        wordDocument.println();
      System.out.println("內存地址:\nwordDocument: "+wordDocument.toString() +"\n" + "cloneDoc: "+cloneDoc.toString());

    }
複製代碼

Output:設計模式

----------------  start  ----------------

txt: 今天是一個好天氣
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

--------------------開始clone-----


 打印副本,看看數據  


----------------  start  ----------------

txt: 今天是一個好天氣
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

  打印修改後的副本  


----------------  start  ----------------

txt: 副奔上修改文檔:老龍王哭了
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

----看會不會影響原始文檔-----


----------------  start  ----------------

txt: 今天是一個好天氣
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------
  
內存地址:
wordDocument: com.devyk.android_dp_code.prototype.WordDocument@48533e64
cloneDoc: com.devyk.android_dp_code.prototype.WordDocument@64a294a6
複製代碼

從上面代碼跟打印能夠看出 cloneDoc 是經過 wordDocument.clone() 建立的而且 cloneDoc 第一次輸出和 wordDocument 原始文檔數據同樣,既 cloneDoc 是 wordDocument 的一份副本文件。難道這樣就完了嗎?不知道你們有沒有注意這裏的 mImagePath 字段,原始對象的 clone 方法這裏至關把引用地址複製給了 clone 出來的對象,若是這 2 個對象中的任意一個對其修改,那麼就會對原始數據形成破壞,失去了對數據的保護。那麼怎麼解決這個問題,請繼續往下瀏覽(注意:經過 clone 的對象並不會執行 構造函數!)安全

淺拷貝和深拷貝

上述原型模式的實現實際上只是一個淺拷貝,也稱爲影子拷貝。這份拷貝實際上並非將原始文檔的全部字段都從新構造了一份,而是副本文檔的字段引用原始文檔的字段。app

162bd6d2b1ba06809.png

咱們知道 A 引用 B 那麼咱們能夠認爲 A,B 都指向同一個地址,當修改 A 時 B 也會隨之改變, B 修改時 A 也會隨之改變。咱們直接看下面代碼示例:ide

@Test
    public void test4() {
        //1. 構建文檔對象
        WordDocument wordDocument = new WordDocument();
        //2. 編輯文檔
        wordDocument.setmTxt("今天是一個好天氣");
        wordDocument.addImagepath("/sdcard/image.png");
        wordDocument.addImagepath("/sdcard/image2.png");
        wordDocument.addImagepath("/sdcard/image3.png");
        //打印文檔內容
        wordDocument.println();


        System.out.println("--------------------開始clone-----\n\n");

        //以原始文檔爲準,copy 副本
        WordDocument cloneDoc = wordDocument.clone();

        System.out.println(" 打印副本,看看數據 \n\n");
        //打印副本,看看數據
        cloneDoc.println();

        //在副本文檔上修改
        cloneDoc.setmTxt("副奔上修改文檔:老龍王哭了");
        cloneDoc.addImagepath("/sdcard/副本發生改變");
        System.out.println(" 打印修改後的副本 \n\n");
        //打印修改後的副本
        cloneDoc.println();
        System.out.println("----看會不會影響原始文檔-----\n\n");
        //看會不會影響原始文檔???????
        wordDocument.println();

        System.out.println("內存地址:\nwordDocument: " + wordDocument.toString() + "\n" + "cloneDoc: " + cloneDoc.toString());

    }
複製代碼

注意看副本文檔,我手動調用 addImagepath 添加了一個新的圖片地址。那麼你們猜原始文檔會發生改變嗎?請看下面的輸出:函數

----------------  start  ----------------

txt: 今天是一個好天氣
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

--------------------開始clone-----


 打印副本,看看數據  


----------------  start  ----------------

txt: 今天是一個好天氣
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

  打印修改後的副本  


----------------  start  ----------------

txt: 副奔上修改文檔:老龍王哭了
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
path: /sdcard/副本發生改變
-----------------  end   ----------------

----看會不會影響原始文檔-----


----------------  start  ----------------

txt: 今天是一個好天氣
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
path: /sdcard/副本發生改變
-----------------  end   ----------------

內存地址:
wordDocument: com.devyk.android_dp_code.prototype.WordDocument@48533e64
cloneDoc: com.devyk.android_dp_code.prototype.WordDocument@64a294a6
複製代碼

注意看咱們副本添加的圖片地址是否是影響了原始文檔的圖片地址數據,那麼這是怎麼回事勒?對 C++ 瞭解的同窗應該深有體會,這是由於上文中 cloneDoc 只是進行了淺拷貝,圖片列表 mImagePath 只是單純的指向了 this.mImagePath , 並無從新構造一個 mImagePath 對象,就像開始介紹淺/深拷貝同樣, A,B 對象其實指向的是同一個地址,因此無論 A,B 中任意一個對象改了指向地址的數據那麼都會隨之發生改變,那如何解決這個問題?答案就是採起深拷貝,即在拷貝對象時,對於引用型的字段也要採用拷貝的形式,而不是單純引用形式,下面咱們修改 clone 代碼,以下:

/** * 聲明具有 clone 能力 * @return clone 的對象 */
    @Override
    public WordDocument clone() {
        try {
            WordDocument document = (WordDocument)super.clone();
            document.mTxt = this.mTxt;
            //進行深拷貝
            document.mImagePath = (ArrayList<String>) this.mImagePath.clone();
            return document;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
複製代碼

再來測試一下,看輸出類容:

----------------  start  ----------------

txt: 今天是一個好天氣
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

--------------------開始clone-----


 打印副本,看看數據  


----------------  start  ----------------

txt: 今天是一個好天氣
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

  打印修改後的副本  


----------------  start  ----------------

txt: 副奔上修改文檔:老龍王哭了
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
path: /sdcard/副本發生改變
-----------------  end   ----------------

----看會不會影響原始文檔-----


----------------  start  ----------------

txt: 今天是一個好天氣
mImagePath: 
path: /sdcard/image.png
path: /sdcard/image2.png
path: /sdcard/image3.png
-----------------  end   ----------------

內存地址:
wordDocument: com.devyk.android_dp_code.prototype.WordDocument@48533e64
cloneDoc: com.devyk.android_dp_code.prototype.WordDocument@64a294a6
複製代碼

經過輸出內容,深拷貝解決了上述問題。

原型模式是一個很是簡單的一個模式,它的核心問題就是對原始對象進行拷貝,在這個模式的使用過程當中須要注意一點就是 深/淺拷貝的問題。在實際開發中,爲了減小沒必要要的麻煩,建議你們都使用深拷貝。

這裏若是對深淺拷貝感興趣的話能夠看掘金上這篇文章,不過是 JS 代碼(瞭解原理就能夠了),很火的一篇文章值得學習一下

源碼中的原型模式

  • ArrayList

    剛剛咱們 clone 文檔可知,進行的 ArrayList clone ,那麼 ArrayList clone 具體是怎麼實現的?咱們一塊兒來看下:

    public Object clone() {
            try {
              //1. 
                ArrayList<?> v = (ArrayList<?>) super.clone();
              //2. 
                v.elementData = Arrays.copyOf(elementData, size);
                v.modCount = 0;
                return v;
            } catch (CloneNotSupportedException e) {
                // this shouldn't happen, since we are Cloneable
                throw new InternalError(e);
            }
        }
    複製代碼

    代碼中第一步首先進行自身的 clone ,而後在對自身的數據進行 copy .

  • Intent

    下面以 Intent 來分析源碼中的原型模式,首先看以下代碼

    public static Intent toSMS(){
            Uri uri = Uri.parse("smsto:11202");
    
            Intent preIntent = new Intent(Intent.ACTION_SENDTO,uri);
            preIntent.putExtra("sms_body","test");
    
            //clone
            return (Intent) preIntent.clone();
    
        }
    
    複製代碼

    2.png

    從代碼中能夠看到 preIntent.clone(); 方法拷貝了一個對象 Intent ,而後執行跳轉 Activity,跳轉的內容與原型數據一致。

    咱們繼續看 Intent clone 具體實現:

    /***進行 clone **/  
    	@Override
        public Object clone() {
            return new Intent(this);
        }
    複製代碼
    /** * Copy constructor. */
        public Intent(Intent o) {
            this.mAction = o.mAction;
            this.mData = o.mData;
            this.mType = o.mType;
            this.mPackage = o.mPackage;
            this.mComponent = o.mComponent;
            this.mFlags = o.mFlags;
            this.mContentUserHint = o.mContentUserHint;
            this.mLaunchToken = o.mLaunchToken;
            if (o.mCategories != null) {
                this.mCategories = new ArraySet<String>(o.mCategories);
            }
            if (o.mExtras != null) {
                this.mExtras = new Bundle(o.mExtras);
            }
            if (o.mSourceBounds != null) {
                this.mSourceBounds = new Rect(o.mSourceBounds);
            }
            if (o.mSelector != null) {
                this.mSelector = new Intent(o.mSelector);
            }
            if (o.mClipData != null) {
                this.mClipData = new ClipData(o.mClipData);
            }
        }
    複製代碼

    能夠看到 clone 方法實際上在內部並無調用 super.clone() 來實現拷貝對象,而是經過 new Intent(this)。 在開始咱們提到過,使用 clone 和 new 須要根據構造對象的成原本決定,若是對象的構形成本比較高或者構造麻煩,那麼使用 clone 函數效率較高,反之可使用 new 關鍵字的形式。這就是和 C++ 中的 copy 構造函數徹底一致,將原始對象做爲構造函數的參數,而後在構造函數內將原始對象數據挨個 copy , 到此,整個 clone 過程就完成了。

總結

原型模式本質就是對象 copy ,與 C++ 中的拷貝構造函數類似,他們以前容易出現的問題也都是深拷貝、淺拷貝。使用原型模式能夠解決構建複雜對象的資源消耗問題,可以在某些場景下提高建立愛你對象的效率。還有一個重要的用途,就是保護性拷貝,也就是某個對象對外可能只是只讀模式。

優勢:

原型模式是在內存中二進制流的 copy, 要比 new 一個對象性能好不少,特別是要在一個循環體內產生大量的對象時,原型模式能夠更好地體現其優勢。

缺點:

這既是它的有點也是缺點,直接在內存中拷貝,構造函數時不會執行的,在實際開發中應該注意這個潛在的問題。

文章代碼地址

特別感謝

《 Android 源碼設計模式解析與實戰 》

相關文章
相關標籤/搜索