設計模式(十六)原型模式

劉望舒發表於2017-06-29

相關文章
設計模式系列

前言

公眾號有同學留言設計模式,才發現好久沒有寫設計模式了。關於建立型設計模式只差原型模式沒寫了,這一篇就來填補這個空缺。

1.原型模式定義

原型模式定義

定義:用原型例項指定建立物件的種類,並通過拷貝這些原型建立新的物件。

原型模式UML圖

在原型模式中有如下角色:

  • Client:客戶端角色。
  • Prototype:抽象原型角色,抽象類或者介面,用來宣告clone方法。
  • ConcretePrototype:具體的原型類,是客戶端角色使用的物件,即被複制的物件。

需要注意的是,Prototype通常是不用自己定義的,因為拷貝這個操作十分常用,Java中就提供了Cloneable介面來支援拷貝操作,它就是原型模式中的Prototype。當然,原型模式也未必非得去實現Cloneable介面,也有其他的實現方式。

2.原型模式簡單實現

原型模式的核心是clone方法,通過該方法進行拷貝,這裡舉一個名片拷貝的例子。
現在已經流行電子名片了,只要掃一下就可以將名片拷貝到自己的名片庫中, 我們先實現名片類。

具體的原型類

public class BusinessCard implements Cloneable {
    private String name;
    private String company;
    public BusinessCard(){
        System.out.println("執行建構函式BusinessCard");
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setCompany(String company) {
        this.company = company;
    }
    @Override
    public BusinessCard clone() {
        BusinessCard businessCard = null;
        try {
            businessCard = (BusinessCard) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return businessCard;
    }
    public void show() {
        System.out.println("name:" + name);
        System.out.println("company:" + company);
    }
}複製程式碼

BusinessCard類實現了Cloneable介面,它是一個標識介面,表示這個物件是可拷貝的,只要重寫clone方法就可以實現拷貝。如果實現了Cloneable介面卻沒有重寫clone方法就會報錯。需要注意的是,clone方法不是在Cloneable介面中定義的(Cloneable介面中沒有定義任何方法),而是在Object中定義的。

客戶端呼叫

public class Client {
    public static void main(String[] args) {
        BusinessCard businessCard = new BusinessCard();
        businessCard.setName("錢三");
        businessCard.setCompany("阿里");
        //拷貝名片
        BusinessCard cloneCard1 = businessCard.clone();
        cloneCard1.setName("趙四");
        cloneCard1.setCompany("百度");

        BusinessCard cloneCard2 = businessCard.clone();
        cloneCard2.setName("孫五");
        cloneCard2.setCompany("騰訊");

        businessCard.show();
        cloneCard1.show();
        cloneCard2.show();
    }
}複製程式碼

除了第一個名片,其他兩個名片都是通過clone方法得到的,需要注意的是,clone方法並不會執行cloneCard1和cloneCard2的建構函式,執行結果為:
執行建構函式BusinessCard
name:錢三
company:阿里
name:趙四
company:百度
name:孫五
company:騰訊

3.淺拷貝和深拷貝

原型模式涉及到淺拷貝和深拷貝的知識點,為了更好的理解它們,還需要舉一些例子。

實現淺拷貝

上述的例子中,BusinessCard的欄位都是String型別的,如果欄位是引用的型別的,會出現什麼情況呢?如下所示。

public class DeepBusinessCard implements Cloneable {
    private String name;
    private Company company = new Company();

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

    public void setCompany(String name, String address) {
        this.company.setName(name);
        this.company.setAddress(address);
    }

    @Override
    public DeepBusinessCard clone() {
        DeepBusinessCard businessCard = null;
        try {
            businessCard = (DeepBusinessCard) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return businessCard;
    }

    public void show() {
        System.out.println("name:" + name);
        System.out.println("company:" + company.getName() + "-address-" + company.getAddress());
    }

}複製程式碼

我們定義了DeepBusinessCard 類,它的欄位company是引用型別的,Company類如下所示。

public class Company {
    private String name;
    private String address;

    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }複製程式碼

在客戶端使用DeepBusinessCard:

public class Client {
    public static void main(String[] args) {
       DeepBusinessCard businessCard=new DeepBusinessCard();
        businessCard.setName("錢三");
        businessCard.setCompany("阿里","北京望京");

        DeepBusinessCard cloneCard1=businessCard.clone();
        cloneCard1.setName("趙四");
        cloneCard1.setCompany("百度","北京西二旗");

        DeepBusinessCard cloneCard2=businessCard.clone();
        cloneCard2.setName("孫五");
        cloneCard2.setCompany("騰訊","北京中關村");

        businessCard.show();
        cloneCard1.show();
        cloneCard2.show();
    }
}複製程式碼

執行結果為:
name:錢三
company:騰訊-address-北京中關村
name:趙四
company:騰訊-address-北京中關村
name:孫五
company:騰訊-address-北京中關村

從結果可以看出company欄位為最後設定的"騰訊"、"北京中關村"。這是因為Object類提供的clone方法,不會拷貝物件中的內部陣列和引用物件,導致它們仍舊指向原來物件的內部元素地址,這種拷貝叫做淺拷貝。
company欄位是引用型別,businessCard被拷貝後,company欄位仍舊指向原來的businessCard物件的company欄位的地址。這樣我們每次設定company欄位,都會覆蓋上一次設定的值,最終留下的就是最後一次設定的值:"騰訊"、"北京中關村"。
引用關係如下圖所示。


這樣的引用關係顯然不符合需求,有多個物件可以修改company,我們應該將引用關係改為如下形式:


拷貝businessCard物件的同時,也將它內部的引用物件company進行拷貝,使得每個拷貝的物件之間無任何關聯,都指向了自身對應的company,這種拷貝就是深拷貝。

實現深拷貝

首先需要修改Company類,如下所示。

public class Company implements Cloneable{
    private String name;
    private String address;
    ...
    public Company clone(){
        Company company=null;
        try {
            company= (Company) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return company;
    }
}複製程式碼

為了實現Company類能被拷貝,Company類也需要實現Cloneable介面並且覆寫clone方法。接著修改DeepBusinessCard的clone方法:

public class DeepBusinessCard implements Cloneable {
    private String name;
    private Company company = new Company();
    ...
    @Override
    public DeepBusinessCard clone() {
        DeepBusinessCard businessCard = null;
        try {
            businessCard = (DeepBusinessCard) super.clone();
            businessCard.company = this.company.clone();//1

        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return businessCard;
    }
  ...
}複製程式碼

在註釋1處增加了對company欄位的拷貝處理。最後在客戶端呼叫,輸出的結果為:
name:錢三
company:阿里-address-北京望京
name:趙四
company:百度-address-北京西二旗
name:孫五
company:騰訊-address-北京中關村

4.原型模式的使用場景

  • 如果類的初始化需要耗費較多的資源,那麼可以通過原型拷貝避免這些消耗。
  • 通過new產生一個物件需要非常繁瑣的資料準備或訪問許可權,則可以使用原型模式。
  • 一個物件需要提供給其他物件訪問,而且各個呼叫者可能都需要修改其值時,可以拷貝多個物件供呼叫者使用,即保護性拷貝。

5.原型模式的優缺點

優點

原型模式是在記憶體中二進位制流的拷貝,要比new一個物件的效能要好,特別是需要產生大量物件時。

缺點

直接在記憶體中拷貝,建構函式是不會執行的,這樣就減少了約束,這既是優點也是缺點,需要在實際應用中去考量。

參考資料
《大話設計模式》
《設計模式之禪》
《Android原始碼設計模式解析與實戰》


歡迎關注我的微信公眾號,第一時間獲得部落格更新提醒,以及更多成體系的Android相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,即可關注。

相關文章