1. 程式人生 > 實用技巧 >【設計模式(四)】原型模式

【設計模式(四)】原型模式

個人學習筆記分享,當前能力有限,請勿貶低,菜鳥互學,大佬繞道

如有勘誤,歡迎指出和討論,本文後期也會進行修正和補充


前言

原型模式是一種建立型設計模式, 使你能夠複製已有物件, 而又無需使程式碼依賴它們所屬的類。

原型(Prototype)模式的定義如下:用一個已經建立的例項作為原型,通過複製該原型物件來建立一個和原型相同或相似的新物件。在這裡,原型例項指定了要建立的物件的種類。用這種方式建立物件非常高效,根本無須知道物件建立的細節。

當直接建立物件的代價比較大時,則採用這種模式。


1.介紹

使用目的:已知原型例項的情況下,可以獲得相同的例項物件

使用時機:需要動態的生成和刪除例項模型

解決問題:動態的建立和刪除例項

實現方法:實現Cloneable類的clone()方法

使用場景

  • 通過new一個物件需要極其繁瑣的資料準備或者許可權,那麼推薦使用原型模式
  • 物件初始化需要消耗大量資源的時候,從舊的物件進行克隆出新物件,即可不必重複初始化
  • 一個物件可能有多個修改者,那麼可以克隆出去多份新物件供其使用

應用例項:

  • 細胞分裂
  • JAVA 中的 Object clone() 方法

優點

  • 效能提高,構建新的物件只需要拷貝舊的物件即可
  • 逃避建構函式的約束,根本不經過建構函式

缺點

  • 配備克隆方法需要對類的功能進行通盤考慮,這對於全新的類不是很難,但對於已有的類不一定很容易,特別當一個類引用不支援序列化的間接物件,或者引用含有迴圈結構的時候
  • 類必須實現 Cloneable 介面。

注意事項:既然是拷貝,那麼必須有源物件才能實現,否則還是得構建一個全新的物件


分類:通過拷貝的方法和其內容,分為淺拷貝和深拷貝

  • 淺拷貝:只拷貝源物件的基本資料,而不拷貝容器,引用等等,一般實現Cloneable類並重寫clone()方法
  • 深拷貝:拷貝源物件的一切,包括資料、容器、引用等等,一般通過實現 Serializable 讀取二進位制流,直接複製出新物件,也可通過其他方式實現

2.實現方案

深拷貝或者淺拷貝是結果,而非簡單的由方案決定,比如在clone方法中拷貝全部內容,也可以達到深拷貝的效果

請注意,示例中修正新物件僅為了測試,實際應用中請視情況處理,理論上應當保持新舊物件儘可能一致

2.1.方案1:實現Cloneable類

實現Cloneable類並重寫clone()方法即可,在該方法中設定好需要拷貝的內容,呼叫源物件中的該方法即可獲得新的物件

步驟

  1. 定義一個實現了Cloneable介面的抽象類,並實現clone()方法

    abstract class Animal implements Cloneable {
        protected String type;
        protected List<String> typeSet = new ArrayList<>();
    
        public Animal(String type) {
            this.type = type;
            typeSet.add(type);
        }
    
        void say() {
            System.out.println("myType is " + type);
            System.out.println("myTypeSet is " + String.join(",", typeSet));
        }
    
        public Animal clone() {
            Animal clone = null;
            try {
                //克隆物件
                clone = (Animal) super.clone();
                //克隆物件裡的複雜物件,若不拷貝則會使用源物件裡的複雜物件
                //clone.typeSet = (List<String>) ((ArrayList) this.typeSet).clone();
    
                //修正新物件
                clone.type = type + "Cloned";
                clone.typeSet.add(clone.type);
    
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return clone;
        }
    }
    

    這裡我們在克隆時,會對新物件進行修正,而其餘內容保持與源物件一致

    複雜物件後面進行測試

  2. 定義實體類,實現抽象類,簡單易懂

    class Dog extends Animal {
    
        public Dog() {
            super("dog");
        }
    
        @Override
        void say() {
            super.say();
            System.out.println("汪!");
        }
    }
    
    class Cat extends Animal {
    
        public Cat() {
            super("cat");
        }
    
        @Override
        void say() {
            super.say();
            System.out.println("喵!");
        }
    }
    
  3. 定義資料來源初始化和呼叫方法

    private Map<String, Animal> animalMap = new HashMap<>();
    
    public void initAnimal() {
        Dog dog = new Dog();
        animalMap.put("dog", dog);
        Cat cat = new Cat();
        animalMap.put("cat", cat);
    }
    
    public Animal getAnimal(String type) {
        if (animalMap.containsKey(type)) {
            return animalMap.get(type).clone();
        } else {
            return null;
        }
    }
    

    initAnimal()方法中初始化出兩個物件,並將其儲存入map

    getAnimal()方法中取出物件的克隆

  4. 測試呼叫

        public static void main(String[] args) {
            //初始化
            initAnimal();
    
            //第一輪測試
            System.out.println("test turn 1:");
            Animal dog1 = getAnimal("dog");
            Animal cat1 = getAnimal("cat");
            dog1.say();
            cat1.say();
    
            //第二輪測試
            System.out.println("test turn 2:");
            Animal dog2 = dog1.clone();
            dog2.say();
    
            //第三輪測試
            System.out.println("test turn 3:");
            dog1.say();
            System.out.println("clone equals:" + Objects.equals(dog1.typeSet, dog2.typeSet));
        }
    

完整程式碼

package com.company.clone;

import java.util.*;

class Animal implements Cloneable {
    protected String type;
    protected List<String> typeSet = new ArrayList<>();

    public Animal(String type) {
        this.type = type;
        typeSet.add(type);
    }

    void say() {
        System.out.println("myType is " + type);
        System.out.println("myTypeSet is " + String.join(",", typeSet));
    }

    public Animal clone() {
        Animal clone = null;
        try {
            //克隆物件
            clone = (Animal) super.clone();
            //克隆物件裡的複雜物件,若不拷貝則會使用源物件裡的複雜物件
            //clone.typeSet = (List<String>) ((ArrayList) this.typeSet).clone();

            //修正新物件
            clone.type = type + "Cloned";
            clone.typeSet.add(clone.type);

        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return clone;
    }
}

class Dog extends Animal {

    public Dog() {
        super("dog");
    }

    @Override
    void say() {
        super.say();
        System.out.println("汪!");
    }
}

class Cat extends Animal {

    public Cat() {
        super("cat");
    }

    @Override
    void say() {
        super.say();
        System.out.println("喵!");
    }
}

public class CloneTest {
    public static void main(String[] args) {
        //初始化
        initAnimal();

        //第一輪測試
        System.out.println("test turn 1:");
        Animal dog1 = getAnimal("dog");
        Animal cat1 = getAnimal("cat");
        dog1.say();
        cat1.say();

        //第二輪測試
        System.out.println("test turn 2:");
        Animal dog2 = dog1.clone();
        dog2.say();

        //第三輪測試
        System.out.println("test turn 3:");
        dog1.say();
        System.out.println("clone equals:" + Objects.equals(dog1.typeSet, dog2.typeSet));
    }

    private static Map<String, Animal> animalMap = new HashMap<>();

    public static void initAnimal() {
        Dog dog = new Dog();
        animalMap.put("dog", dog);
        Cat cat = new Cat();
        animalMap.put("cat", cat);
    }

    public static Animal getAnimal(String type) {
        if (animalMap.containsKey(type)) {
            return animalMap.get(type).clone();
        } else {
            return null;
        }
    }
}

執行結果

分析

  • 第一輪測試為一次克隆的結果,type後追加ClonedtypeSet包括2個數據,分別是舊type和新type
  • 第二輪測試為二次克隆的結果,type後追加兩個ClonedtypeSet包括3個數據
  • 第三輪仍為一次克隆的結果,type後只追加了一個Cloned,但typeSet卻有3個數據,與二次克隆是同一個typeSet

也就是說克隆後,使用的typeSet是同一個

這也就是所說的淺拷貝:只拷貝資料,而容器、引用等則直接使用源物件的,並不進行拷貝

如果需要深拷貝,則需要在clone()方法中對複雜物件進行復制

如果啟用clone()方法中註釋掉的程式碼,執行結果會變成下面這樣

可以看到現在的typeSet不是同一個了,只是拷貝的時候複製了裡面的內容


2.2.方案2:序列化後進行復制

即將物件以二進位制完全複製一份新的,再轉換為物件,通常使用Serializable和資料流一起實現

步驟

這裡僅修改clone()方法,其餘步驟與方案1一致

class Animal implements Serializable {
    protected String type;
    protected List<String> typeSet = new ArrayList<>();

    public Animal(String type) {
        this.type = type;
        typeSet.add(type);
    }

    void say() {
        System.out.println("myType is " + type);
        System.out.println("myTypeSet is " + String.join(",", typeSet));
    }

    public Animal clone() {
        Animal clone = null;
        try {
            //將物件寫到流裡
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            //從流裡讀回來
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            clone = (Animal) ois.readObject();
            //修正新物件
            clone.type = type + "Cloned";
            clone.typeSet.add(clone.type);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return clone;
    }
}

將物件寫入流裡,再讀出來,並強制轉換為對應類的物件即可

執行結果

分析

可以看到結果是深拷貝,因為新物件時由資料流轉換來的,複雜物件保留的是資料,而非引用地址,那麼自然會構造新的


3.後記

例項中我們預先初始化所有源物件,使用map儲存,使用的時候取出對應物件的克隆體

其實這就是最常見的應用場景,我們可以快速獲得對應的物件,不需要每次都初始化和進行構造,而對深拷貝物件的修改都不會影響源物件,也就可以保證每次的源物件是相同且純淨的

一次初始化,之後便可快速得到新的物件,我們也可以在初始化的時候進行資料預設等等,視業務情況處理

總之,還是很實用的對吧~



作者:Echo_Ye

WX:Echo_YeZ

EMAIL :[email protected]

個人站點:在搭了在搭了。。。(右鍵 - 新建資料夾)