1. 程式人生 > >設計模式 | 原型模式及典型應用

設計模式 | 原型模式及典型應用

前言

本文的主要內容如下:

  • 介紹原型模式
  • 示例
    • Java語言的clone
    • 淺克隆與深克隆
    • 實現深克隆
  • 原型模式的典型應用

原型模式

原型模式(Prototype Pattern):使用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。原型模式是一種物件建立型模式。

原型模式的工作原理很簡單:將一個原型物件傳給那個要發動建立的物件,這個要發動建立的物件通過請求原型物件拷貝自己來實現建立過程。

原型模式是一種“另類”的建立型模式,建立克隆物件的工廠就是原型類自身,工廠方法由克隆方法來實現。

需要注意的是通過克隆方法所建立的物件是全新的物件,它們在記憶體中擁有新的地址,通常對克隆所產生的物件進行修改對原型物件不會造成任何影響,每一個克隆物件都是相互獨立的。通過不同的方式修改可以得到一系列相似但不完全相同的物件。

角色

  • Prototype(抽象原型類):它是宣告克隆方法的介面,是所有具體原型類的公共父類,可以是抽象類也可以是介面,甚至還可以是具體實現類。
  • ConcretePrototype(具體原型類):它實現在抽象原型類中宣告的克隆方法,在克隆方法中返回自己的一個克隆物件。
  • Client(客戶類):讓一個原型物件克隆自身從而建立一個新的物件,在客戶類中只需要直接例項化或通過工廠方法等方式建立一個原型物件,再通過呼叫該物件的克隆方法即可得到多個相同的物件。由於客戶類針對抽象原型類Prototype程式設計,因此使用者可以根據需要選擇具體原型類,系統具有較好的可擴充套件性,增加或更換具體原型類都很方便。

原型模式的核心在於如何實現克隆方法

示例

Java語言提供的clone()方法

學過Java語言的人都知道,所有的Java類都繼承自 java.lang.Object。事實上,Object 類提供一個 clone() 方法,可以將一個Java物件複製一份。因此在Java中可以直接使用 Object 提供的 clone() 方法來實現物件的克隆,Java語言中的原型模式實現很簡單。

需要注意的是能夠實現克隆的Java類必須實現一個 標識介面 Cloneable,表示這個Java類支援被複制。如果一個類沒有實現這個介面但是呼叫了clone()方法,Java編譯器將丟擲一個 CloneNotSupportedException

異常。

public class Mail implements Cloneable{
    private String name;
    private String emailAddress;
    private String content;
    public Mail(){
        System.out.println("Mail Class Constructor");
    }
    // ...省略 getter、setter
    @Override
    protected Object clone() throws CloneNotSupportedException {
        System.out.println("clone mail object");
        return super.clone();
    }
}

在客戶端建立原型物件和克隆物件也很簡單,如下程式碼所示:

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Mail mail = new Mail();
        mail.setContent("初始化模板");
        System.out.println("初始化mail:"+mail);
        for(int i = 0;i < 3;i++){
            System.out.println();
            Mail mailTemp = (Mail) mail.clone();
            mailTemp.setName("姓名"+i);
            mailTemp.setEmailAddress("姓名"+i+"@test.com");
            mailTemp.setContent("恭喜您,此次抽獎活動中獎了");
            MailUtil.sendMail(mailTemp);
            System.out.println("克隆的mailTemp:"+mailTemp);
        }
        MailUtil.saveOriginMailRecord(mail);
    }
}

其中的 MailUtil 工具類為

public class MailUtil {
    public static void sendMail(Mail mail) {
        String outputContent = "向{0}同學,郵件地址:{1},郵件內容:{2}傳送郵件成功";
        System.out.println(MessageFormat.format(outputContent, mail.getName(), mail.getEmailAddress(), mail.getContent()));
    }

    public static void saveOriginMailRecord(Mail mail) {
        System.out.println("儲存originMail記錄,originMail:" + mail.getContent());
    }
}

輸出如下:

Mail Class Constructor
初始化mail:Mail{name='null', emailAddress='null', content='初始化模板'}com.designpattern.prototype.Mail@12edcd21

clone mail object
向姓名0同學,郵件地址:姓名0@test.com,郵件內容:恭喜您,此次抽獎活動中獎了傳送郵件成功
克隆的mailTemp:Mail{name='姓名0', emailAddress='姓名[email protected]', content='恭喜您,此次抽獎活動中獎了'}com.designpattern.prototype.Mail@34c45dca

clone mail object
向姓名1同學,郵件地址:姓名1@test.com,郵件內容:恭喜您,此次抽獎活動中獎了傳送郵件成功
克隆的mailTemp:Mail{name='姓名1', emailAddress='姓名[email protected]', content='恭喜您,此次抽獎活動中獎了'}com.designpattern.prototype.Mail@52cc8049

clone mail object
向姓名2同學,郵件地址:姓名2@test.com,郵件內容:恭喜您,此次抽獎活動中獎了傳送郵件成功
克隆的mailTemp:Mail{name='姓名2', emailAddress='姓名[email protected]', content='恭喜您,此次抽獎活動中獎了'}com.designpattern.prototype.Mail@5b6f7412
儲存originMail記錄,originMail:初始化模板

從輸出結果中我們可以觀察到:

  • for迴圈中的 mailTemp 從 mail 物件中克隆得到,它們的記憶體地址均不同,說明不是同一個物件,克隆成功,克隆僅僅通過呼叫 super.clone() 即可。
  • 最後呼叫的 MailUtil.saveOriginMailRecord(mail); 中的 mail 物件的內容仍為 for 迴圈之前設定的內容,並沒有因為克隆而改變。
  • 克隆的時候呼叫了 clone 方法,並沒有呼叫 Mail 類的構造器,只在最前面 new 的時候才呼叫了一次

關於輸出的記憶體地址是怎麼輸出的,我們還需要看一下 Object#toString 方法

public class Object {
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    //...省略...
}

所以所謂的記憶體地址即為 hashCode() 的十六進位制表示,這裡簡單的認為 記憶體地址相同則為同一個物件,不同則為不同物件

再來看一眼 Object#clone 方法

protected native Object clone() throws CloneNotSupportedException;

這是一個 native 關鍵字修飾的方法

一般而言,Java語言中的clone()方法滿足:

  • 對任何物件x,都有 x.clone() != x,即克隆物件與原型物件不是同一個物件;
  • 對任何物件x,都有 x.clone().getClass() == x.getClass(),即克隆物件與原型物件的型別一樣;
  • 如果物件x的 equals() 方法定義恰當,那麼 x.clone().equals(x) 應該成立。

為了獲取物件的一份拷貝,我們可以直接利用Object類的clone()方法,具體步驟如下:

  1. 在派生類中覆蓋基類的 clone() 方法,並宣告為public;
  2. 在派生類的 clone() 方法中,呼叫 super.clone()
  3. 派生類需實現Cloneable介面。

此時,Object類相當於抽象原型類,所有實現了Cloneable介面的類相當於具體原型類

淺克隆與深克隆

看下面的示例

public class Pig implements Cloneable{
    private String name;
    private Date birthday;
    // ...getter, setter, construct
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Pig pig = (Pig)super.clone();
        return pig;
    }
    @Override
    public String toString() {
        return "Pig{" +
                "name='" + name + '\'' +
                ", birthday=" + birthday +
                '}'+super.toString();
    }
}

測試

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Date birthday = new Date(0L);
        Pig pig1 = new Pig("佩奇",birthday);
        Pig pig2 = (Pig) pig1.clone();
        System.out.println(pig1);
        System.out.println(pig2);

        pig1.getBirthday().setTime(666666666666L);

        System.out.println(pig1);
        System.out.println(pig2);
    }
}

輸出如下

Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@27973e9b
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@312b1dae
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@27973e9b
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@312b1dae

我們照著上一小節說的實現 Cloneable,呼叫 super.clone(); 進行克隆,中間我們對 pig1 物件設定了一個時間戳,從輸出中我們可以發現什麼問題呢?

我們可以發現:

  • pig1pig2 的記憶體地址不同
  • pig1 設定了時間,同事 pig2 的時間也改變了

我們通過 debug 來看一下

debug檢視物件地址

發現如下:

  • pig1 與 pig2 地址不一樣
  • pig1 的 birthday 與 pig2 的 birthday 一樣

這裡引出淺拷貝與深拷貝。

在Java語言中,資料型別分為值型別(基本資料型別)和引用型別,值型別包括int、double、byte、boolean、char等簡單資料型別,引用型別包括類、介面、陣列等複雜型別。

淺克隆和深克隆的主要區別在於是否支援引用型別的成員變數的複製,下面將對兩者進行詳細介紹。

淺克隆:

  • 在淺克隆中,如果原型物件的成員變數是值型別,將複製一份給克隆物件;如果原型物件的成員變數是引用型別,則將引用物件的地址複製一份給克隆物件,也就是說原型物件和克隆物件的成員變數指向相同的記憶體地址。

  • 簡單來說,在淺克隆中,當物件被複制時只複製它本身和其中包含的值型別的成員變數,而引用型別的成員物件並沒有複製

  • 在Java語言中,通過覆蓋Object類的clone()方法可以實現淺克隆。

深克隆:

  • 在深克隆中,無論原型物件的成員變數是值型別還是引用型別,都將複製一份給克隆物件,深克隆將原型物件的所有引用物件也複製一份給克隆物件。

  • 簡單來說,在深克隆中,除了物件本身被複制外,物件所包含的所有成員變數也將複製。

  • 在Java語言中,如果需要實現深克隆,可以通過序列化(Serialization)等方式來實現。需要注意的是能夠實現序列化的物件其類必須實現Serializable介面,否則無法實現序列化操作。

實現深克隆

方式一,手動對引用物件進行克隆:

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Pig pig = (Pig)super.clone();

        //深克隆
        pig.birthday = (Date) pig.birthday.clone();
        return pig;
    }

方式二,通過序列化的方式:

public class Pig implements Serializable {
    private String name;
    private Date birthday;
    // ...省略 getter, setter等

    protected Object deepClone() throws CloneNotSupportedException, IOException, ClassNotFoundException {
        //將物件寫入流中
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(this);

        //將物件從流中取出
        ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (ois.readObject());
    }
}

序列化方式的深克隆結果

破壞單例模式

餓漢式單例模式如下:

public class HungrySingleton implements Serializable, Cloneable {

    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {
        if (hungrySingleton != null) {
            throw new RuntimeException("單例構造器禁止反射呼叫");
        }
    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
    private Object readResolve() {
        return hungrySingleton;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

使用反射獲取物件,測試如下

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        Method method = hungrySingleton.getClass().getDeclaredMethod("clone");
        method.setAccessible(true);
        HungrySingleton cloneHungrySingleton = (HungrySingleton) method.invoke(hungrySingleton);
        System.out.println(hungrySingleton);
        System.out.println(cloneHungrySingleton);
    }
}

輸出

com.designpattern.HungrySingleton@34c45dca
com.designpattern.HungrySingleton@52cc8049

可以看到,通過原型模式,我們把單例模式給破壞了,現在有兩個物件了

為了防止單例模式被破壞,我們可以:不實現 Cloneable 介面;或者把 clone 方法改為如下

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }

原型模式的典型應用

  1. Object 類中的 clone 介面
  2. Cloneable 介面的實現類,可以看到至少一千多個,找幾個例子譬如:

Cloneable介面的實現類

ArrayListclone 的重寫如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            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);
        }
    }
    //...省略
}

呼叫 super.clone(); 之後把 elementData 資料 copy 了一份

同理,我們看看 HashMapclone 方法的重寫:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    @Override
    public Object clone() {
        HashMap<K,V> result;
        try {
            result = (HashMap<K,V>)super.clone();
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
        result.reinitialize();
        result.putMapEntries(this, false);
        return result;
    }
    // ...省略...
}

mybatis 中的 org.apache.ibatis.cache.CacheKeyclone 方法的重寫:

public class CacheKey implements Cloneable, Serializable {
    private List<Object> updateList;
    public CacheKey clone() throws CloneNotSupportedException {
        CacheKey clonedCacheKey = (CacheKey)super.clone();
        clonedCacheKey.updateList = new ArrayList(this.updateList);
        return clonedCacheKey;
    }
    // ... 省略...
}

這裡又要注意,updateListList<Object> 型別,所以可能是值型別的List,也可能是引用型別的List,克隆的結果需要注意是否為深克隆或者淺克隆

使用原始模式的時候一定要注意為深克隆還是淺克隆。

原型模式總結

原型模式的主要優點如下:

  • 當建立新的物件例項較為複雜時,使用原型模式可以簡化物件的建立過程,通過複製一個已有例項可以提高新例項的建立效率。
  • 擴充套件性較好,由於在原型模式中提供了抽象原型類,在客戶端可以針對抽象原型類進行程式設計,而將具體原型類寫在配置檔案中,增加或減少產品類對原有系統都沒有任何影響。
  • 原型模式提供了簡化的建立結構,工廠方法模式常常需要有一個與產品類等級結構相同的工廠等級結構,而原型模式就不需要這樣,原型模式中產品的複製是通過封裝在原型類中的克隆方法實現的,無須專門的工廠類來建立產品。
  • 可以使用深克隆的方式儲存物件的狀態,使用原型模式將物件複製一份並將其狀態儲存起來,以便在需要的時候使用(如恢復到某一歷史狀態),可輔助實現撤銷操作。

原型模式的主要缺點如下:

  • 需要為每一個類配備一個克隆方法,而且該克隆方法位於一個類的內部,當對已有的類進行改造時,需要修改原始碼,違背了“開閉原則”。
  • 在實現深克隆時需要編寫較為複雜的程式碼,而且當物件之間存在多重的巢狀引用時,為了實現深克隆,每一層物件對應的類都必須支援深克隆,實現起來可能會比較麻煩。

適用場景:

  • 建立新物件成本較大(如初始化需要佔用較長的時間,佔用太多的CPU資源或網路資源),新的物件可以通過原型模式對已有物件進行復制來獲得,如果是相似物件,則可以對其成員變數稍作修改。
  • 如果系統要儲存物件的狀態,而物件的狀態變化很小,或者物件本身佔用記憶體較少時,可以使用原型模式配合備忘錄模式來實現。
  • 需要避免使用分層次的工廠類來建立分層次的物件,並且類的例項物件只有一個或很少的幾個組合狀態,通過複製原型物件得到新例項可能比使用建構函式建立一個新例項更加方便。

參考:
劉偉:設計模式Java版
慕課網java設計模式精講 Debug 方式+記憶體分析

推薦閱讀

設計模式 | 簡單工廠模式及典型應用
設計模式 | 工廠方法模式及典型應用
設計模式 | 抽象工廠模式及典型應用
設計模式 | 建造者模式及典型應用

更多內容請訪問我的個人部落格:http://laijianfeng.org/

關注_小旋鋒_微信公眾號_及時接收博文推送