23種設計模式(八)-原型設計模式

一. 什麼是原型設計模式?

1.1 原型設計模式的概念

​ 原型設計模式的思想類似於我們常用的複製粘貼功能. 把一個地方的文件複製到另外一個地方, 複製完以後, 兩個文件的內容是一模一樣的. 原型設計模式的精髓也在於此. 原型模式用於創建重複的對象,首先創建一個實例對象, 然後通過拷貝實例對象創建一個新的對象。這種模式類似於創建型模式。

使用原型模式創建對象非常高效,無須知道對象創建的細節.多用於創建複雜的或者構造耗時的實例,因為在這種情況下,複製一個已經存在的實例會更高效。

1.2 為什麼要使用原型設計模式?

  1. 通常, 類初始化的過程需要消耗很多的資源,這個資源包括數據、空間、時間資源等,通過原型拷貝降低這樣的消耗

  2. 通過new 去創建一個對象,需要非常繁瑣的步驟,如:數據準備和檢查訪問許可權等。使用原型模式可以簡化這些操作。

  3. 當一個對象需要被其他對象訪問或者操作時, 如果各個調用者都修改數據的可能性,那麼這時可以考慮原型設計模式拷貝多個對象以供調用者使用,即保護性複製。

原型設計模式通常使用在new一個資源很耗時的情況,相比new,使用原型模式的效率明顯提高。

二. 原型設計模式實現步驟

2.1 原型設計模式的結構

下面來看原型模式的UML圖:

從上圖中可以看出原型設計模式的結構構成:

  1. 抽象原型類: 定義了具體原型對象鼻血實現的介面. 這裡是Cloneable介面
  2. 具體原型類: 實現了抽象原型類, 並重寫了clone方法.這個類的對象就是可被複制的對象.
  3. 訪問類: 實現使用具體原型類克隆出新類的類

2.2 原型模式實現的步驟

原型模式主要用於對象的複製, 他的核心是Propototype原型類, 下面來看看在java中, 實現原型模式的步驟:

第一步: 原型類Prototype實現Cloneable介面,
第二步: 重寫Object的clone()方法.
第三步: 在目標類也就是PrototypeTest類型調用Prototype類的clone方法, 實現對象的複製.

Cloneable介面: 在Java語言中有一個自帶的Cloneable介面, 這個介面的作用只有一個, 就是在程式運行的時候通知虛擬機可以安全的在實現了此介面的類上使用clone()方法. 在java虛擬機中, 只有實現了這個介面的類才能被拷貝, 否則會拋出異常CloneNotSupportedException.

public interface Cloneable {
}

重寫Object的clone()方法: 在java中所有的類都有一個父類Object , Object裡面定義了一個clone()方法, 但是這個clone()方法是protected類型的, 在其他地方不能隨便使用, 所以, 我們需要重寫clone方法, 並將其作用域設置為public.

public class Object {
    protected native Object clone() throws CloneNotSupportedException;
}

原型模式很少單獨出現。經常與其他模式混用,他的原型類Prototype也常用抽象類來替代。

三. 原型設計模式的案例

我們就以上面的UML為例, 來感受一下原型設計模式拷貝出的對象

  1. Prototype原型類, 實現了Cloneable介面, 並重寫了Object的clone()方法
public class Prototype implements Cloneable{

    private String name;

    private int age;

    private String sex;

    public Prototype(String name, int age, String sex) {
        this.name = name;
        this.age = age;
        this. sex = sex;
    }


    /**
     * 重寫object的clone()方法, 並將其作用域設置為public
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Prototype{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}
  1. PrototypeTest類, 通過調用Prototype的clone()方法來克隆已經創建的Prototype對象.
public class PrototypeTest {

    public static void main(String[] args) throws CloneNotSupportedException {
        Prototype prototype = new Prototype("張三", 8, "男");
        Prototype cloneObject = (Prototype)prototype.clone();

        System.out.println(cloneObject);
    }
}

運行結果

Prototype{name=’張三’, age=8, sex=’男’}

四. 原型設計模式實現的類型.

原型設計模式實現的類型有兩種: 淺拷貝深拷貝

4.1 淺拷貝

淺拷貝指的是在創建一個對象的時候, 新對象的屬性和原來對象的屬性完全相同, 對於非基本類型屬性, 扔指向原有屬性所指向的對象的記憶體地址。

還是用上面的demo來說明:

public class Prototype implements Cloneable{

    private String name;

    private int age;

    private String sex;

    private ArrayList<String> hobbies;

    public Prototype(String name, int age, String sex, ArrayList<String> hobbies) {
        this.name = name;
        this.age = age;
        this. sex = sex;
        this.hobbies = hobbies;
    }



    /**
     * 重寫object的clone()方法, 並將其作用域設置為public
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public Prototype clone() throws CloneNotSupportedException {
        Prototype clone = (Prototype)super.clone();
        
        return clone;
    }
}

public class PrototypeTest {

    public static void main(String[] args) throws CloneNotSupportedException {
        ArrayList hobbies = new ArrayList();
        hobbies.add("籃球");
        hobbies.add("排期");
        Prototype prototype = new Prototype("張三", 8, "男", hobbies);
        Prototype cloneObject = (Prototype)prototype.clone();

        System.out.println("比較克隆前後的對象:"+(prototype == cloneObject));
        System.out.println("比較克隆前後的List<String>屬性:" + (prototype.getHobbies() == cloneObject.getHobbies()));

    }
}

運行結果:

比較克隆前後的對象:false
比較克隆前後的List屬性:true

我們在比較hobbies的時候, 使用的是 的含義是地址和值都一樣才返回true。

第一個返回結果是false。說明克隆後重新創建了一個對象。

第二個結果返回的是ture,說明克隆後引用類型的對象指向了原來對象的地址。

這是一種淺拷貝, 默認的拷貝方式是淺拷貝

4.2 深拷貝

深拷貝是指在創建一個對象的時候, 屬性中引用的其他對象也會被克隆,不再指向原有的地址。

我們還是使用上面的案例, 將上面的淺拷貝變成深拷貝

public class Prototype implements Cloneable{

    private String name;
    private int age;
    private String sex;
    private ArrayList<String> hobbies;

    public Prototype(String name, int age, String sex, ArrayList<String> hobbies) {
        this.name = name;
        this.age = age;
        this. sex = sex;
        this.hobbies = hobbies;
    }

    /**
     * 重寫object的clone()方法, 並將其作用域設置為public
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public Prototype clone() throws CloneNotSupportedException {
        Prototype clone = (Prototype)super.clone();
        System.out.println("淺拷貝:" + (clone.hobbies == this.hobbies));
        clone.hobbies = (ArrayList<String>) (this.hobbies).clone();
        System.out.println("深拷貝:" + (clone.hobbies == this.hobbies));
        return clone;
    }

    @Override
    public String toString() {
        return "Prototype{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}


public class PrototypeTest {

    public static void main(String[] args) throws CloneNotSupportedException {
        ArrayList hobbies = new ArrayList();
        hobbies.add("籃球");
        hobbies.add("排期");
        Prototype prototype = new Prototype("張三", 8, "男", hobbies);
        Prototype cloneObject = (Prototype)prototype.clone();

        System.out.println("比較克隆前後的對象:"+(prototype == cloneObject));
        System.out.println("比較克隆前後的List<String>屬性:" + (prototype.getHobbies() == cloneObject.getHobbies()));
    }
}

運行結果:

淺拷貝:true
深拷貝:false
比較克隆前後的對象:false
比較克隆前後的List屬性:false

這是一種深拷貝, 實現方式是

public Prototype clone() throws CloneNotSupportedException {
        Prototype clone = (Prototype)super.clone();
        System.out.println("淺拷貝:" + (clone.hobbies == this.hobbies));
        clone.hobbies = (ArrayList<String>) (this.hobbies).clone();
        System.out.println("深拷貝:" + (clone.hobbies == this.hobbies));
        return clone;
    }

對ArrayList使用了clone。這樣就保障對象重新創建。

原型模式是比較簡單的一個模式,核心就是對原始對象進行拷貝,為了減少錯誤,建議每次都進行深拷貝

五. 原型設計模式的優缺點

5.1 優點

在記憶體中二進位流進行拷貝,要比直接new 一個對象性能要好。

  1. 如果創建新的對象比較複雜時,可以利用原型模式簡化對象的創建過程,同時也能提高效率
  2. 可以使用深拷貝保持原始對象的狀態
  3. 原型模式提供了簡化的創建結構

5.2 缺點

直接在記憶體中進行拷貝,是不會執行構造函數的,減少了約束。優點是減少了約束,缺點也是減少了約束。

  1. 在實現深拷貝的時候可能需要比較複雜的程式碼
  2. 需要為每一個類配備一個clone 方法,而且這個clone 方法需要對類的整體功能進行考慮,這對全新的類來說並不困難,但對已有的類進行改造時,並不是一件容易的事,必須修改其源程式碼,違背了「開閉原則」

六. 原型模式的注意事項

使用原型模式複製對象不會調用類的構造方法。因為對象的複製是通過調用Object類的clone方法來完成的,它直接在記憶體中複製數據,因此不會調用到類的構造方法。

不但構造方法中的程式碼不會執行,甚至連訪問許可權都對原型模式無效。還記得單例模式嗎?單例模式中,只要將構造方法的訪問許可權設置為private型,就可以實現單例。但是clone方法直接無視構造方法的許可權,所以,單例模式與原型模式是衝突的,在使用時要特別注意。

深拷貝與淺拷貝。Object類的clone方法只會拷貝對象中的基本的數據類型,對於數組、容器對象、引用對象等都不會拷貝,這就是淺拷貝。如果要實現深拷貝,必須將原型模式中的數組、容器對象、引用對象等另行拷貝。例如

public class Prototype implements Cloneable{

    private String name;
    private int age;
    private String sex;
    private ArrayList<String> hobbies;

    /**
     * 重寫object的clone()方法, 並將其作用域設置為public
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public Prototype clone() throws CloneNotSupportedException {
        Prototype clone = (Prototype)super.clone();
        clone.hobbies = (ArrayList<String>) (this.hobbies).clone();
        return clone;
    }
}

由於ArrayList不是基本類型,所以成員變數list,不會被拷貝,需要我們自己實現深拷貝,幸運的是java提供的大部分的容器類都實現了Cloneable介面。所以實現深拷貝並不是特別困難。

七. 原型設計模式的應用場景

7.1 原型模式適用的場景:

  1. 通常, 一個類很複雜的時候, 包含了很多種數據和結構,數據結構層次又比較深時,適用於原型模式。
  2. 當複雜的對象需要獨立於系統運行, 而不能破壞本系統中的結構時。

7.2 實例場景:

  1. 一個樓盤有名稱,地址和施工隊三個成員變數。施工隊有名稱,人數和包工頭。包工頭有名稱和年齡。現在要建設一個隔壁的樓盤,還是由這個施工隊進行建設的,只是地址不同。如果重新創建,過程較為複雜,費時費力,採取原型模式可以快速構建一個新的樓盤。
  2. 系統中已經有一架飛機,飛機有名稱和型號和廠商。廠商有名稱,地址和負責人。負責人有姓名和年齡。現在要一家相同的飛機由不同的負責人進行指導生產的,如何快速創建這樣的對象。

八. 原型設計模式運用了那幾大原則

我們來盤點一下設計模式的六大原則, 看看上面的原型模式案例應用了設計模式六大原則的哪幾類模型

  1. 單一職責原則:類很簡單, 符合
  2. 里式替換原則:重寫了父類的clone方法,不符合
  3. 介面隔離原則:最小介面原則,符合
  4. 依賴倒置原則:不依賴於具體,依賴於抽象,介面隔離原則實現了cloneable介面,符合
  5. 迪米特法則:類之間有最好了聯繫。借口隔離原則很簡單,也符合
  6. 開放封閉原則:對擴展開放,對修改關閉。符合