設計模式學習筆記(七):原型模式

1 概述

1.1 引言

對於某些崗位來講,工做週報的內容會大同小異,若是用戶每次都須要從空白的週報進行輸入無疑會浪費用戶不少的時間,若是週報可以按照用戶的自定義來生成模板,或者從已有模板修改小部分獲得新模板,這樣用戶的輸入效率會大大提升。原型模式正是爲解決這類問題而生。java

1.2 定義

原型模式:使用原型實例指定建立對象的種類,而且經過克隆這些原型建立新的對象。apache

原型模式是一種對象建立型模式。編程

原型模式的工做原理很簡單,將一個原型對象傳給那個要發動建立的對象,這個要發動建立的對象經過請求原型對象克隆本身來實現建立過程。原型模式是一種另類的建立型模式,建立克隆對象的工廠就是原型類自身,工廠方法由克隆方法實現。數組

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

1.3 結構圖

在這裏插入圖片描述

1.4 角色

  • Prototype(抽象原型類):聲明克隆方法的接口,是全部具體原型類的公共父類,能夠是抽象類也能夠是接口,還能是具體實現類
  • ConcretePrototypr(具體原型類):實如今抽象原型類中聲明的克隆方法,在克隆方法中返回本身的一個克隆對象
  • Client(客戶類):讓一個原型對象克隆自身從而建立一個新的對象,在客戶類中只須要直接實例化或經過工廠方法等方式建立一個原型對象,再經過調用該對象的克隆方法便可獲得多個相同的對象。

2 典型實現

2.1 步驟

  • 定義抽象原型類:定義爲接口/抽象類,至少須要定義一個相似clone的方法
  • 定義具體原型類:實現/繼承抽象原型類,核心是實現其中的clone
  • 定義客戶類:針對抽象原型類編程,首先須要經過實例化或工廠方法等建立一個原型對象,接着經過其中的clone方法獲取多個對象

2.2 抽象原型類

這裏定義爲接口:網絡

interface Prototype
{
    Prototype clone();
    String getAttr();
    void setAttr(String attr);
}

2.3 具體原型類

實現抽象原型接口,核心在於如何實現clone,在Java中clone一般有兩種實現方式:ide

  • 通用實現方法
  • clone()方法

2.3.1 通用實現方法

通用的克隆實現方法是在具體原型類的克隆方法中實例化一個與自身類型相同的對象並將其返回,並將相關的參數傳入新建立的對象中,保證成員變量相同。函數

代碼以下:工具

class ConcretePrototype implements Prototype
{
    private String attr;

    @Override
    public String getAttr() {
        return this.attr;
    }

    @Override
    public void setAttr(String attr) {
        this.attr = attr;
    }
    
    @Override
    public Prototype clone()
    {
        Prototype = new ConcretePrototype();
        prototype.setAttr(attr);
        return prototype;
    }
}

2.3.2 clone

java.lang.Object提供了一個clone(),能夠將一個Java對象克隆一份,利用clone()能夠直接將對象克隆一份,可是必須實現Cloneable接口,不然clone()時會拋出CloneNotSupportedException測試

代碼以下:

class ConcretePrototype implements Prototype,Cloneable
{
    private String attr;

    public String getAttr() {
        return this.attr;
    }

    public void setAttr(String attr) {
        this.attr = attr;
    }
    
    public Prototype clone()
    {
        Object object = null;
        try 
        {
            object = super.clone();
        } 
        catch (Exception e) 
        {
            e.printStacktrace();
        }
        return (Prototype)object;
    }
}

通常而言,Java中的clone()知足:

  • 對任何對象x都有x.clone() != x,也就是克隆的對象與原型對象不是同一個對象
  • 對任何對象x都有x.clone().getClass() == x.getClass(),即克隆對象與原型對象的類型同樣
  • 若是xequals()定義恰當,那麼x.clone().equals(x)應該成立

具體實現步驟以下:

  • 覆蓋clone(),並聲明爲public
  • clone()中調用super.clone()
  • 派生類須要實現Cloneable接口

2.4 客戶類

客戶類針對抽象原型類編程,經過實例化獲取具體原型後,調用其中的clone進行克隆:

public class Test
{
    public static void main(String[] args) {
        Prototype prototype1 = new ConcretePrototype();
        prototype1.setAttr("test");
        Prototype prototype2 = prototype1.clone();
        System.out.println(prototype1.getAttr() == prototype2.getAttr());
        System.out.println(prototype1 == prototype2);
    }
}

3 實例

開發一個工做週報系統,工做週報的內容都大同小異,只有一些小地方存在差別,可是系統每次默認建立的都是空白報表,用戶不斷複製粘貼來填寫重複內容。使用原型模式對其進行優化,快速建立相同或相似的工做週報。

設計以下:

  • 抽象原型類:無(也能夠認爲是Object
  • 具體原型類:WeeklyLog

代碼以下:

public class Test
{
    public static void main(String[] args) {
        WeeklyLog weeklyLog1 = new WeeklyLog();
        weeklyLog1.setContent("content");
        weeklyLog1.setName("Weekly log 1");
        weeklyLog1.setDateTime(LocalDateTime.now());

        System.out.println(weeklyLog1.getName());
        System.out.println(weeklyLog1.getContent());
        System.out.println(weeklyLog1.getDateTime());

        WeeklyLog weeklyLog2 = weeklyLog1.clone();
        weeklyLog2.setName("Weekly log 2");
        System.out.println(weeklyLog2.getName());
        System.out.println(weeklyLog2.getContent());
        System.out.println(weeklyLog2.getDateTime());
    }
}

class WeeklyLog implements Cloneable
{
    private String name;
    private LocalDateTime dateTime;
    private String content;

    //getter and setter
    //...

    public WeeklyLog clone()
    {
        Object obj = null;
        try
        {
            obj = super.clone();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        return (WeeklyLog)obj;
    }
}

4 淺克隆與深克隆

通常來講,工做週報可能會攜帶附件,使用上面的原型模式來進行工做週報的複製沒有問題,可是附件(通常是另外一個類)不會進行復制。這是由於淺克隆與深克隆的緣由,下面具體來看一下。

4.1 淺克隆

在淺克隆中,若是原型對象的成員變量是值類型,將複製一份給克隆對象。(在Java中)值類型包括:

  • int
  • double
  • byte
  • boolean
  • char
  • float
  • long
  • short

也就是這些類型的值都會完整複製一份給克隆對象,對於引用類型,則將引用對象的地址複製一份給克隆對象。(在Java中)引用類型就是除了基本類型以外的全部類型,常見的有:

  • 接口
  • 數組

對於引用類型,原型對象與克隆對象指向相同的內存地址,也就是其實並無被複制,而是共享一份地址相同的值。
在Java中能夠經過Objectclone()實現淺克隆,也就是上面例子的作法。

4.2 深克隆

在深克隆中,不管變量是值類型仍是引用類型都會完整複製一份給克隆對象。

在Java中實現深克隆能夠經過序列化等方式實現。序列化就是將對象寫到流的過程,寫到流中的對象是原有對象的一個複製品,而原對象仍然存在於內存中。想要進行序列化必須實現Serializable接口。

代碼以下:

public class Test
{
    public static void main(String[] args) {
        WeeklyLog weeklyLog1 = new WeeklyLog();
        WeeklyLog weeklyLog2 = null;
        Attachement attachement = new Attachement();
        weeklyLog1.setAttachement(attachement);
        try
        {
            weeklyLog2 = weeklyLog1.deepClone(); 
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        System.out.println(weeklyLog1 == weeklyLog2);
        System.out.println(weeklyLog1.getAttachement() == weeklyLog2.getAttachement());
    }
}

class Attachement implements Serializable
{
    private String name;
    //getter and setter
    //...
}

class WeeklyLog implements Serializable
{
    private String name;
    private LocalDateTime dateTime;
    private String content;
    private Attachement attachement;

    //getter and setter
    //...

    public WeeklyLog deepClone() throws IOException , ClassNotFoundException , OptionalDataException
    {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(this);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);

        return (WeeklyLog)objectInputStream.readObject();
    }
}

固然除了使用ByteArrayOutput/InputStream以及ObjectInput/OutputStream外,還能夠利用如下工具類進行深克隆:

  • org.apache.commons.lang3.SerializationUtils.clone():須要實現Serializable接口
  • Gson:無需實現Serializable接口,toJson()+fromJson()
  • Jackson:也是無需實現Serializable接口,readValue()+writeValueAsString()

5 原型管理器

5.1 定義

原型管理器是將多個原型對象存儲在一個集合中供客戶端使用的專門負責克隆對象的工廠,其中定義了一個集合用於存儲原型對象,若是須要某個原型對象的克隆,能夠經過複製集合中對應的原型對象來獲取。在原型管理器中針對抽象原型類進行編程。
結構圖以下:
在這裏插入圖片描述

5.2 實例

平常辦公中會有許多公文須要建立,例如《可行性分析報告》,《立項建議書》,《軟件需求規格說明書》,《項目進展報告》等,爲了提升工做效率須要爲各種公文建立模板,用戶能夠經過這些模板快速建立新的公文,這些公文模板進行統一的管理,系統根據用戶的請求的不一樣生成不一樣的新公文。

首先是抽象原型以及具體原型的代碼:

interface OfficialDocument extends Cloneable
{
    OfficialDocument clone();
    void display();
}

//可行性分析報告
class FAR implements OfficialDocument 
{
    public OfficialDocument clone()
    {
        OfficialDocument far = null;
        try
        {
            far = (OfficialDocument)super.clone();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        return far;
    }

    public void display()
    {
        System.out.println("可行性分析報告");
    }
}

//軟件需求規格說明書
class SRS implements OfficialDocument
{
    public OfficialDocument clone()
    {
        OfficialDocument srs = null;
        try
        {
            srs = (OfficialDocument)super.clone();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
        return srs;
    }

    public void display()
    {
        System.out.println("軟件需求規格說明書");
    }
}

接着是原型管理器的代碼,使用枚舉單例實現:

enum PrototypeManager 
{
    INSTANCE;

    private Hashtable<String,OfficialDocument> hashtable = new Hashtable<>();
    private PrototypeManager()
    {
        add("far",new FAR());
        add("srs",new SRS());
    }

    public void add(String key,OfficialDocument document)
    {
        hashtable.put(key, document);
    }

    public OfficialDocument get(String key)
    {
        return ((OfficialDocument)hashtable.get(key)).clone();
    }
}

測試代碼:

public class Test
{
    public static void main(String[] args) {
        PrototypeManager manager = PrototypeManager.INSTANCE;
        OfficialDocument document1,document2,document3,document4;

        document1 = manager.get("far");
        document1.display();

        document2 = manager.get("far");
        document2.display();
        System.out.println(document1 == document2);

        document3 = manager.get("srs");
        document3.display();

        document4 = manager.get("srs");
        document4.display();
        System.out.println(document3 == document4);
    }
}

6 主要優勢

  • 簡化建立過程:當建立新的對象實例較爲複雜時,使用原型模式能夠簡化對象的建立過程,經過複製一個已有實例能夠提升新實例的建立效率
  • 擴展性較好:因爲在原型模式中提供了抽象原型類,在客戶端能夠針對抽象原型類進行編程,而將具體原型類寫在配置文件中,增長或減小具體原型類對系統都沒有任何影響
  • 簡化建立結構:原型模式提供了簡化的建立結構。工廠方法模式經常須要有一個與產品類等級結構相同的工廠等級結構,而原型模式就不須要這樣,原型模式中產品的複製是經過封裝在原型類中的克隆方法實現的,無需專門的工廠類來建立產品
  • 保存狀態:可使用深克隆的方式保存對象的狀態。使用原型模式將對象複製一份並將其狀態保存起來,以便在須要的時候使用,例如恢復到某一歷史狀態,可輔助實現撤銷操做

7 主要缺點

  • 修改不方便:須要爲每個類配備一個克隆方法,並且該克隆方法位於一個類的內部,當對已有的類進行改造的時候,須要修改源代碼,違背了OCP(開放閉合原則)
  • 深克隆須要嵌套類支持:在實現深克隆時須要編寫較爲複雜的代碼,並且當對象之間存在多重的嵌套引用時,爲了實現深克隆,每一層對象都必須支持深克隆,實現起來可能比較麻煩

8 適用場景

  • 建立新對象成本較大,好比初始化須要較長時間,佔用太多的CPU資源或網絡資源,新的對象能夠經過原型模式對已有對象進行復制獲取,若是是類似對象能夠對成員變量稍做修改
  • 若是系統要保存對象的狀態,而對象的變化狀態很小,或者對象自己佔用內存較少,可使用原型模式配合備忘錄模式
  • 須要避免使用分層次的工廠類來建立分層次的對象,而且類的實例對象只有一個或不多的幾個組合狀態,經過複製原型對象獲得新實例可能比使用構造函數建立一個實例方便

9 總結

在這裏插入圖片描述

若是以爲文章好看,歡迎點贊。

同時歡迎關注微信公衆號:氷泠之路。

在這裏插入圖片描述

相關文章
相關標籤/搜索