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

前言

本文的主要內容以下:php

  • 介紹原型模式css

  • 示例java

    • Java語言的cloneapache

    • 淺克隆與深克隆編程

    • 實現深克隆設計模式

  • 原型模式的典型應用數組

原型模式

原型模式(Prototype Pattern):使用原型實例指定建立對象的種類,而且經過拷貝這些原型建立新的對象。原型模式是一種對象建立型模式。微信

原型模式的工做原理很簡單:將一個原型對象傳給那個要發動建立的對象,這個要發動建立的對象經過請求原型對象拷貝本身來實現建立過程。網絡

原型模式是一種「另類」的建立型模式,建立克隆對象的工廠就是原型類自身,工廠方法由克隆方法來實現。mybatis

須要注意的是經過克隆方法所建立的對象是全新的對象,它們在內存中擁有新的地址,一般對克隆所產生的對象進行修改對原型對象不會形成任何影響,每個克隆對象都是相互獨立的。經過不一樣的方式修改能夠獲得一系列類似但不徹底相同的對象。

角色

  • 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='姓名0@test.com', content='恭喜您,這次抽獎活動中獎了'}com.designpattern.prototype.Mail@34c45dca

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

clone mail object
向姓名2同窗,郵件地址:姓名2@test.com,郵件內容:恭喜您,這次抽獎活動中獎了發送郵件成功
克隆的mailTemp:Mail{name='姓名2', emailAddress='姓名2@test.com', 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 SerializableCloneable {

    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<Eextends AbstractList<E>
        implements List<E>, RandomAccessCloneablejava.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,Vextends AbstractMap<K,Vimplements Map<K,V>, CloneableSerializable {
    @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(thisfalse);
        return result;
    }
    // ...省略...
}

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

public class CacheKey implements CloneableSerializable {
    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 方式+內存分析

推薦閱讀

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

關注_小旋鋒_微信公衆號


本文分享自微信公衆號 - 小旋鋒(whirlysBigData)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索