【白話設計模式十二】原型模式(Prototype)

#0 系列目錄#java

#1 場景問題# ##1.1 訂單處理系統## 考慮這樣一個實際應用:訂單處理系統。算法

如今有一個訂單處理的系統,裏面有個保存訂單的業務功能,在這個業務功能裏面,客戶有這麼一個需求:每當訂單的預約產品數量超過1000的時候,就須要把訂單拆成兩份訂單來保存,若是拆成兩份訂單後,仍是超過1000,那就繼續拆分,直到每份訂單的預約產品數量不超過1000。至於爲何要拆分,緣由是好進行訂單的後續處理,後續是由人工來處理,每一個人工工做小組的處理能力上限是1000。編程

根據業務,目前的訂單類型被分紅兩種:一種是我的訂單,一種是公司訂單。如今想要實現一個通用的訂單處理系統,也就是說,無論具體是什麼類型的訂單,都要可以正常的處理。設計模式

該怎麼實現呢?緩存

##1.2 不用模式的解決方案## 來分析上面要求實現的功能,有朋友會想,這很簡單嘛,一共就一個功能,沒什麼困難的,真的是這樣嗎?先來嘗試着實現看看。多線程

  1. 定義訂單接口

首先,要想實現通用的訂單處理,而不關心具體的訂單類型,那麼很明顯,訂單處理的對象應該面向一個訂單的接口或是一個通用的訂單對象來編程,這裏就選用面向訂單接口來處理吧,先把這個訂單接口定義出來,示例代碼以下:工具

/**
 * 訂單的接口
 */
public interface OrderApi {
    /**
     * 獲取訂單產品數量
     * @return 訂單中產品數量
     */
    public int getOrderProductNum();
    /**
     * 設置訂單產品數量
     * @param num 訂單產品數量
     */
    public void setOrderProductNum(int num);
}
  1. 既然定義好了訂單的接口,那麼接下來把各類類型的訂單實現出來,先看看我的的訂單實現,示例代碼以下:
/**
 * 我的訂單對象
 */
public class PersonalOrder implements OrderApi{
    /**
     * 訂購人員姓名
     */
    private String customerName;
    /**
     * 產品編號
     */
    private String productId;
    /**
     * 訂單產品數量
     */
    private int orderProductNum = 0;

    public int getOrderProductNum() {
       return this.orderProductNum;
    }  
    public void setOrderProductNum(int num) {
       this.orderProductNum = num;
    }  
    public String getCustomerName() {
       return customerName;
    }
    public void setCustomerName(String customerName) {
       this.customerName = customerName;
    }
    public String getProductId() {
       return productId;
    }
    public void setProductId(String productId) {
       this.productId = productId;
    }
    public String toString(){
       return "本我的訂單的訂購人是="+this.customerName+",訂購產品是="+this.productId+",訂購數量爲="+this.orderProductNum;
    }
}

再看看企業訂單的實現,示例代碼以下:學習

/**
 * 企業訂單對象
 */
public class EnterpriseOrder implements OrderApi{
    /**
     * 企業名稱
     */
    private String enterpriseName;
    /**
     * 產品編號
     */
    private String productId;  
    /**
     * 訂單產品數量
     */
    private int orderProductNum = 0;

    public int getOrderProductNum() {
       return this.orderProductNum;
    }  
    public void setOrderProductNum(int num) {
       this.orderProductNum = num;
    }  
    public String getEnterpriseName() {
       return enterpriseName;
    }
    public void setEnterpriseName(String enterpriseName) {
       this.enterpriseName = enterpriseName;
    }
    public String getProductId() {
       return productId;
    }
    public void setProductId(String productId) {
       this.productId = productId;
    }
    public String toString(){
       return "本企業訂單的訂購企業是="+this.enterpriseName+",訂購產品是="+this.productId+",訂購數量爲="+this.orderProductNum;
    }
}

有些朋友看到這裏,可能會有這樣的疑問:看上去上面兩種類型的訂單對象,僅僅是一個數據封裝的對象,並且還有一些數據是相同的,爲什麼不抽出一個父類來,把共同的數據定義在父類裏面呢?測試

這裏有兩個考慮,一個是:這裏僅僅是一個示意,實際狀況遠比這複雜,實際開發中不會僅僅是數據封裝對象這麼簡單。另一個是:爲了後續示例的重點突出,這裏要學習的是原型模式,所以就沒有去抽取父類,以避免對象層級過多,影響主題的展現。ui

  1. 實現好了訂單對象,接下來看看如何實現通用的訂單處理,先把訂單處理的對象大概定義出來,示例代碼以下:
/**
 * 處理訂單的業務對象
 */
public class OrderBusiness {
    /**
     * 建立訂單的方法
     * @param order 訂單的接口對象
     */
    public void saveOrder(OrderApi order){
        //等待具體實現
    }
}

如今的中心任務就是要來實現這個saveOrder的方法,傳入的參數是一個訂單的接口對象,這個方法要實現的功能:根據業務要求,當訂單的預約產品數量超過1000的時候,就須要把訂單拆成兩份訂單。

那好,來嘗試着實現一下,由於預約的數量可能會很大,所以採用一個while循環來處理,直到拆分後訂單的數量不超過1000,先把實現的思路寫出來,示例代碼以下:

public class OrderBusiness {
    public void saveOrder(OrderApi order){
        //1:判斷當前的預約產品數量是否大於1000
        while(order.getOrderProductNum() > 1000){
            //2:若是大於,還須要繼續拆分
            //2.1再新建一份訂單,跟傳入的訂單除了數量不同外,其它都相同
            OrderApi newOrder = null;

            ...
        }
    }
}

你們會發現,纔剛寫到第二步就寫不下去了,爲何呢?由於如今判斷須要拆分訂單,也就是須要新建一個訂單對象,但是訂單處理對象面對的是訂單的接口,它根本就不知道如今訂單具體的類型,也不知道具體的訂單實現,它沒法建立出新的訂單對象來,也就沒法實現訂單拆分的功能了

  1. 一個簡單的解決辦法

有朋友提供了這麼一個解決的思路,他說:不就是在saveOrder方法裏面不知道具體的類型,從而致使沒法建立對象嗎?很簡單,使用instanceof來判斷不就能夠了,他還給出了他的實現示意,示意代碼以下:

public class OrderBusiness {
    public void saveOrder(OrderApi order){
       while(order.getOrderProductNum() > 1000)
           //定義一個表示被拆分出來的新訂單對象
           OrderApi newOrder = null;         
           if(order instanceof PersonalOrder){
              //建立相應的訂單對象
              PersonalOrder p2 = new PersonalOrder();
              //而後進行賦值等,省略了
              //而後再設置給newOrder
              newOrder = p2;
           }else if(order instanceof EnterpriseOrder){
               //建立相應的訂單對象
              EnterpriseOrder e2 = new EnterpriseOrder();
              //而後進行賦值等,省略了
              //而後再設置給newOrder
              newOrder = e2;
           }         
           //而後進行拆分和其它業務功能處理,省略了
       }     
    }
}

好像能解決問題,對吧。那咱們就來按照他提供的思路,把這個通用的訂單處理對象實現出來,示例代碼以下:

/**
 * 處理訂單的業務對象
 */
public class OrderBusiness {
    /**
     * 建立訂單的方法
     * @param order 訂單的接口對象
     */
    public void saveOrder(OrderApi order){
       //根據業務要求,當訂單預約產品數量超過1000時,就要把訂單拆成兩份訂單
       //固然若是要作好,這裏的1000應該作成常量,這麼作是爲了演示簡單

       //1:判斷當前的預約產品數量是否大於1000
       while(order.getOrderProductNum() > 1000){
           //2:若是大於,還須要繼續拆分
           //2.1再新建一份訂單,跟傳入的訂單除了數量不同外,其它都相同
           OrderApi newOrder = null;
           if(order instanceof PersonalOrder){
              //建立相應的新的訂單對象
              PersonalOrder p2 = new PersonalOrder();
              //而後進行賦值,可是產品數量爲1000
              PersonalOrder p1 = (PersonalOrder)order;
              p2.setCustomerName(p1.getCustomerName());
              p2.setProductId(p1.getProductId());          
              p2.setOrderProductNum(1000);
              //而後再設置給newOrder
              newOrder = p2;
           }else if(order instanceof EnterpriseOrder){
              //建立相應的訂單對象
              EnterpriseOrder e2 = new EnterpriseOrder();
              //而後進行賦值,可是產品數量爲1000
              EnterpriseOrder e1 = (EnterpriseOrder)order;
              e2.setEnterpriseName(e1.getEnterpriseName());
              e2.setProductId(e1.getProductId());
              e2.setOrderProductNum(1000);
              //而後再設置給newOrder
              newOrder = e2;
           }         

           //2.2原來的訂單保留,把數量設置成減小1000
           order.setOrderProductNum(order.getOrderProductNum()-1000);

           //而後是業務功能處理,省略了,打印輸出,看一下
           System.out.println("拆分生成訂單=="+newOrder);
       }     
       //3:不超過1000,那就直接業務功能處理,省略了,打印輸出,看一下
       System.out.println("訂單=="+order);   
    }
}
  1. 寫個客戶端來測試一下,示例代碼以下:
public class OrderClient {
    public static void main(String[] args) {
       //建立訂單對象,這裏爲了演示簡單,直接new了
       PersonalOrder op = new PersonalOrder();
       //設置訂單數據
       op.setOrderProductNum(2925);
       op.setCustomerName("張三");
       op.setProductId("P0001");

       //這裏獲取業務處理的類,也直接new了,爲了簡單,連業務接口都沒有作
       OrderBusiness ob = new OrderBusiness();
       //調用業務來保存訂單對象
       ob.saveOrder(op);
    }
}

運行結果以下:

拆分生成訂單==本我的訂單的訂購人是=張三,訂購產品是=P0001,訂購數量爲=1000
拆分生成訂單==本我的訂單的訂購人是=張三,訂購產品是=P0001,訂購數量爲=1000
訂單==本我的訂單的訂購人是=張三,訂購產品是=P0001,訂購數量爲=925

根據訂單中訂購產品的數量,一份訂單被拆分紅了三份。一樣的,你還能夠傳入企業訂單,看看是否能正常知足功能要求。

##1.3 有何問題## 看起來,上面的實現確實不難,好像也可以通用的進行訂單處理,而不須要關心訂單的類型和具體實現這樣的功能。

仔細想一想,真的沒有關心訂單的類型和具體實現嗎?答案是「否認的」。

事實上,在實現訂單處理的時候,上面的實現是按照訂單的類型和具體實現來處理的,就是instanceof的那一段。有朋友可能會問,這樣實現有何不可嗎?這樣的實現有以下幾個問題:

既然想要實現通用的訂單處理,那麼對於訂單處理的實現對象,是不該該知道訂單的具體實現的,更不該該依賴訂單的具體實現。可是上面的實現中,很明顯訂單處理的對象依賴了訂單的具體實現對象。

這種實現方式另一個問題就是:難以擴展新的訂單類型。假如如今要加入一個大客戶專用訂單的類型,那麼就須要修改訂單處理的對象,要在裏面添加對新的訂單類型的支持,這算哪門子的通用處理。

所以,上面的實現是不太好的,把上面的問題再抽象描述一下:已經有了某個對象實例後,如何可以快速簡單地建立出更多的這種對象?好比上面的問題,就是已經有了訂單接口類型的對象實例,而後在方法中須要建立出更多的這種對象。怎麼解決呢?

#2 解決方案# ##2.1 原型模式來解決## 用來解決上述問題的一個合理的解決方案就是原型模式。那麼什麼是原型模式呢?

  1. 原型模式定義

輸入圖片說明

  1. 應用原型模式來解決的思路

仔細分析上面的問題,在saveOrder方法裏面,已經有了訂單接口類型的對象實例,是從外部傳入的,可是這裏只是知道這個實例對象的種類是訂單的接口類型,並不知道其具體的實現類型,也就是不知道它究竟是我的訂單仍是企業訂單,可是如今須要在這個方法裏面建立一個這樣的訂單對象,看起來就像是要經過接口來建立對象同樣。

原型模式就能夠解決這樣的問題,原型模式會要求對象實現一個能夠「克隆」自身的接口,這樣就能夠經過拷貝或者是克隆一個實例對象自己,來建立一個新的實例。若是把這個方法定義在接口上,看起來就像是經過接口來建立了新的接口對象

這樣一來,經過原型實例建立新的對象,就再也不須要關心這個實例自己的類型,也不關心它的具體實現,只要它實現了克隆自身的方法,就能夠經過這個方法來獲取新的對象,而無須再去經過new來建立。

##2.2 模式結構和說明##

輸入圖片說明

Prototype:聲明一個克隆自身的接口,用來約束想要克隆本身的類,要求它們都要實現這裏定義的克隆方法。

ConcretePrototype:實現Prototype接口的類,這些類真正實現了克隆自身的功能。

Client:使用原型的客戶端,首先要獲取到原型實例對象,而後經過原型實例克隆自身來建立新的對象實例。

##2.3 原型模式示例代碼##

  1. 先來看看原型接口的定義,示例代碼以下:
/**
 * 聲明一個克隆自身的接口
 */
public interface Prototype {
    /**
     * 克隆自身的方法
     * @return 一個從自身克隆出來的對象
     */
    public Prototype clone();
}
  1. 接下來看看具體的原型實現對象,示例代碼以下:
/**
 * 克隆的具體實現對象
 */
public class ConcretePrototype1 implements Prototype {
    public Prototype clone() {
       //最簡單的克隆,新建一個自身對象,因爲沒有屬性,就不去複製值了
       Prototype prototype = new ConcretePrototype1();
       return prototype;
    }
}

/**
 * 克隆的具體實現對象
 */
public class ConcretePrototype2 implements Prototype {
    public Prototype clone() {
       //最簡單的克隆,新建一個自身對象,因爲沒有屬性,就不去複製值了
       Prototype prototype = new ConcretePrototype2();
       return prototype;
    }
}

爲了跟上面原型模式的結構示意圖保持一致,所以這兩個具體的原型實現對象,都沒有定義屬性。事實上,在實際使用原型模式的應用中,原型對象可能是有屬性的,克隆原型的時候也是須要克隆原型對象的屬性的,特此說明一下

  1. 再看看使用原型的客戶端,示例代碼以下:
/**
 * 使用原型的客戶端
 */
public class Client {
    /**
     * 持有須要使用的原型接口對象
     */
    private Prototype prototype;
    /**
     * 構造方法,傳入須要使用的原型接口對象
     * @param prototype 須要使用的原型接口對象
     */
    public Client(Prototype prototype){
       this.prototype = prototype;
    }
    /**
     * 示意方法,執行某個功能操做
     */
    public void operation(){
       //會須要建立原型接口的對象
       Prototype newPrototype = prototype.clone();
    }
}

##2.4 使用原型模式重寫示例## 要使用原型模式來重寫示例,先要在訂單的接口上定義出克隆的接口,而後要求各個具體的訂單對象克隆自身,這樣就能夠解決:在訂單處理對象裏面經過訂單接口來建立新的訂單對象的問題。

使用原型模式來重寫示例的結構如圖9.2所示:

輸入圖片說明

  1. 複製誰和誰來複制的問題

有了一個對象實例,要快速的建立跟它同樣的實例,最簡單的辦法就是複製?這裏又有兩個小的問題:

複製誰呢?固然是複製這個對象實例,複製實例的意思是連帶着數據一塊兒複製。

誰來複制呢?應該讓這個類的實例本身來複制,本身複製本身。

但是每一個對象不會那麼聽話,本身去實現複製本身的。因而原型模式決定對這些對象實行強制要求,給這些對象定義一個接口,在接口裏面定義一個方法,這個方法用來要求每一個對象實現本身複製本身

因爲如今存在訂單的接口,所以就把這個要求克隆自身的方法定義在訂單的接口裏面,示例代碼以下:

/**
 * 訂單的接口,聲明瞭能夠克隆自身的方法
 */
public interface OrderApi {
    public int getOrderProductNum();
    public void setOrderProductNum(int num);
    /**
     * 克隆方法
     * @return 訂單原型的實例
     */
    public OrderApi cloneOrder();
}
  1. 如何克隆

定義好了克隆的接口,那麼在訂單的實現類裏面,就得讓它實現這個接口,並具體的實現這個克隆方法,新的問題出來了,如何實現克隆呢?

很簡單,只要先new一個本身對象的實例,而後把本身實例中的數據取出來,設置到新的對象實例中去,不就能夠完成實例的複製了嘛,複製的結果就是有了一個跟自身如出一轍的實例。

/**
 * 我的訂單對象
 */
public class PersonalOrder implements OrderApi{
    private String customerName;
    private String productId;
    private int orderProductNum = 0;

    public int getOrderProductNum() {
       return this.orderProductNum;
    }  
    public void setOrderProductNum(int num) {
       this.orderProductNum = num;
    }  
    public String getCustomerName() {
       return customerName;
    }
    public void setCustomerName(String customerName) {
       this.customerName = customerName;
    }
    public String getProductId() {
       return productId;
    }
    public void setProductId(String productId) {
       this.productId = productId;
    }
    public String toString(){
       return "本我的訂單的訂購人是="+this.customerName+",訂購產品是="+this.productId+",訂購數量爲="+this.orderProductNum;
    }  
    public OrderApi cloneOrder() {
       //建立一個新的訂單,而後把本實例的數據複製過去
       PersonalOrder order = new PersonalOrder();
       order.setCustomerName(this.customerName);
       order.setProductId(this.productId);
       order.setOrderProductNum(this.orderProductNum);

       return order;
    }
}

接下來看看企業訂單的具體實現,示例代碼以下:

/**
 * 企業訂單對象
 */
public class EnterpriseOrder implements OrderApi{
    private String enterpriseName;
    private String productId;  
    private int orderProductNum = 0;
    public int getOrderProductNum() {
       return this.orderProductNum;
    }  
    public void setOrderProductNum(int num) {
       this.orderProductNum = num;
    }  
    public String getEnterpriseName() {
       return enterpriseName;
    }
    public void setEnterpriseName(String enterpriseName) {
       this.enterpriseName = enterpriseName;
    }
    public String getProductId() {
       return productId;
    }
    public void setProductId(String productId) {
       this.productId = productId;
    }
    public String toString(){
       return "本企業訂單的訂購企業是="+this.enterpriseName+",訂購產品是="+this.productId+",訂購數量爲="+this.orderProductNum;
    }
    public OrderApi cloneOrder() {
       //建立一個新的訂單,而後把本實例的數據複製過去
       EnterpriseOrder order = new EnterpriseOrder();
       order.setEnterpriseName(this.enterpriseName);
       order.setProductId(this.productId);
       order.setOrderProductNum(this.orderProductNum);
       return order;
    }  
}
  1. 使用克隆方法

這裏使用訂單接口的克隆方法的,是訂單的處理對象,也就是說,訂單的處理對象就至關於原型模式結構中的Client。

固然,客戶端在調用clone方法以前,還須要先得到相應的實例對象,有了實例對象,才能調用該實例對象的clone方法。

這裏使用克隆方法的時候,跟標準的原型實現有一些不一樣,在標準的原型實現的示例代碼裏面,客戶端是持有須要克隆的對象,而這裏變化成了經過方法傳入須要使用克隆的對象,這點你們注意一下。示例代碼以下:

public class OrderBusiness {
    /**
     * 建立訂單的方法
     * @param order 訂單的接口對象
     */
    public void saveOrder(OrderApi order){
       //1:判斷當前的預約產品數量是否大於1000
       while(order.getOrderProductNum() > 1000){
           //2:若是大於,還須要繼續拆分
           //2.1再新建一份訂單,跟傳入的訂單除了數量不同外,其它都相同
           OrderApi newOrder = order.cloneOrder();
           //而後進行賦值,產品數量爲1000
           newOrder.setOrderProductNum(1000);

           //2.2原來的訂單保留,把數量設置成減小1000
           order.setOrderProductNum(order.getOrderProductNum()-1000);

           //而後是業務功能處理,省略了,打印輸出,看一下
           System.out.println("拆分生成訂單=="+newOrder);
       }     
       //3:不超過,那就直接業務功能處理,省略了,打印輸出,看一下
       System.out.println("訂單=="+order);
    }
}

##2.5 小提示## 看到這裏,可能有些朋友會認爲:Java的Object裏面自己就有clone方法,還用搞得這麼麻煩嗎?

雖然Java裏面有clone方法,上面這麼作仍是頗有意義的,可讓咱們更好的、更完整的體會原型這個設計模式。固然,後面會講述如何使用Java裏面的clone方法來實現克隆,不要着急。

#3 模式講解# ##3.1 認識原型模式##

  1. 原型模式的功能

原型模式的功能實際上包含兩個方面:

一個是經過克隆來建立新的對象實例;

另外一個是爲克隆出來的新的對象實例複製原型實例屬性的值;

原型模式要實現的主要功能就是:經過克隆來建立新的對象實例。通常來說,新建立出來的實例的數據是和原型實例同樣的。可是具體如何實現克隆,須要由程序自行實現,原型模式並無統一的要求和實現算法。

  1. 原型與new

原型模式從某種意義上說,就像是new操做,在前面的例子實現中,克隆方法就是使用new來實現的,但請注意,只是「相似於new」而不是「就是new」。

克隆方法和new操做最明顯的不一樣就在於:new一個對象實例,通常屬性是沒有值的,或者是隻有默認值;若是是克隆獲得的一個實例,一般屬性是有值的,屬性的值就是原型對象實例在克隆的時候,原型對象實例的屬性的值

  1. 原型實例和克隆的實例

原型實例和克隆出來的實例,本質上是不一樣的實例,克隆完成後,它們之間是沒有關聯的,若是克隆完成後,克隆出來的實例的屬性的值發生了改變,是不會影響到原型實例的。下面寫個示例來測試一下,示例代碼以下:

public class Client {
    public static void main(String[] args) {
       //先建立原型實例
       OrderApi oa1 = new PersonalOrder();

       //設置原型實例的訂單數量的值
       oa1.setOrderProductNum(100);
       //爲了簡單,這裏僅僅輸出數量
       System.out.println("這是第一次獲取的對象實例==="+oa1.getOrderProductNum());

       //經過克隆來獲取新的實例
       OrderApi oa2 = (OrderApi)oa1.cloneOrder();
       //修改它的數量
       oa2.setOrderProductNum(80);
       //輸出克隆出來的對象的值
       System.out.println("輸出克隆出來的實例==="+oa2.getOrderProductNum());

       //再次輸出原型實例的值
       System.out.println("再次輸出原型實例==="+oa1.getOrderProductNum());  
    }
}

運行一下,看看結果:

這是第一次獲取的對象實例===100
輸出克隆出來的實例===80
再次輸出原型實例===100

仔細觀察上面的結果,會發現原型實例和克隆出來的實例是徹底獨立的,也就是它們指向不一樣的內存空間。由於克隆出來的實例的值已經被改變了,而原型實例的值仍是原來的值,並無變化,這就說明兩個實例是對應的不一樣內存空間。

  1. 原型模式的調用順序示意圖

輸入圖片說明

##3.2 Java中的克隆方法## 在Java語言中已經提供了clone方法,定義在Object類中。關於Java中clone方法的知識,這裏不去贅述,下面看看怎麼使用Java裏面的克隆方法來實現原型模式。

須要克隆功能的類,只須要實現java.lang.Cloneable接口,這個接口沒有須要實現的方法,是一個標識接口。所以在前面的實現中,把訂單接口中的克隆方法去掉,如今直接實現Java中的接口就行了。新的訂單接口實現,示例代碼以下:

public interface OrderApi {
    public int getOrderProductNum();
    public void setOrderProductNum(int num);
    // public OrderApi cloneOrder();
}

另外在具體的訂單實現對象裏面,實現方式上會有一些改變,我的訂單和企業訂單的克隆實現是相似的,所以示範一個就行了,看看我的訂單的實現吧,示例代碼以下:

/**
 * 我的訂單對象,利用Java的Clone功能
 */
public class PersonalOrder implements Cloneable  , OrderApi {
    private String customerName;
    private String productId;
    private int orderProductNum = 0;
    public int getOrderProductNum() {
        return this.orderProductNum;
    }  
    public void setOrderProductNum(int num) {
        this.orderProductNum = num;
    }  
    public String getCustomerName() {
        return customerName;
    }
    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }
    public String getProductId() {
        return productId;
    }
    public void setProductId(String productId) {
        this.productId = productId;
    }
    public String toString(){
        return "本我的訂單的訂購人是="+this.customerName+",訂購產品是="+this.productId+",訂購數量爲="+this.orderProductNum;
    }
    // public OrderApi cloneOrder() {
        // 建立一個新的訂單,而後把本實例的數據複製過去
    //    PersonalOrder order = new PersonalOrder();
    //    order.setCustomerName(this.customerName);
    //    order.setProductId(this.productId);
    //    order.setOrderProductNum(this.orderProductNum);     
    //    return order;
    // }
    public Object clone(){
        //克隆方法的真正實現,直接調用父類的克隆方法就能夠了
        Object obj = null;
        try {
            obj = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

看起來,比徹底由本身實現原型模式要稍稍簡單點,是否好用呢?仍是測試一下,看看效果。客戶端跟上一個示例相比,做了兩點修改:

一個是原來的「OrderApi oa1 = new PersonalOrder();」這句話,要修改爲:「PersonalOrder oa1 = new PersonalOrder();」。緣由是如今的接口上並無克隆的方法,所以須要修改爲原型的類型;

另一個是「經過克隆來獲取新的實例」的實現,須要修改爲使用原型來調用在Object裏面定義的clone()方法了,再也不是調用原來的cloneOrder()了。

看看測試用的代碼,示例代碼以下:

public class Client {
    public static void main(String[] args) {
        //先建立原型實例
        PersonalOrder oa1 = new PersonalOrder();     
        //設置原型實例的訂單數量的值
        oa1.setOrderProductNum(100);
        System.out.println("這是第一次獲取的對象實例==="+oa1.getOrderProductNum());      
        //經過克隆來獲取新的實例
        PersonalOrder oa2 = (PersonalOrder)oa1.clone();
        oa2.setOrderProductNum(80);
        System.out.println("輸出克隆出來的實例==="+oa2.getOrderProductNum());     
        //再次輸出原型實例的值
        System.out.println("再次輸出原型實例==="+oa1.getOrderProductNum());  
    }
}

##3.3 淺度克隆和深度克隆## 不管你是本身實現克隆方法,仍是採用Java提供的克隆方法,都存在一個淺度克隆和深度克隆的問題,那麼什麼是淺度克隆?什麼是深度克隆呢?簡單地解釋一下:

淺度克隆:只負責克隆按值傳遞的數據(好比:基本數據類型、String類型)

深度克隆:除了淺度克隆要克隆的值外,還負責克隆引用類型的數據,基本上就是被克隆實例全部的屬性的數據都會被克隆出來。

深度克隆還有一個特色,若是被克隆的對象裏面的屬性數據是引用類型,也就是屬性的類型也是對象,那麼須要一直遞歸的克隆下去。這也意味着,要想深度克隆成功,必需要整個克隆所涉及的對象都要正確實現克隆方法,若是其中有一個沒有正確實現克隆,那麼就會致使克隆失敗。

在前面的例子中實現的克隆就是典型的淺度克隆,下面就來看看如何實現深度克隆。

  1. 本身實現原型的深度克隆

(1)要演示深度克隆,須要給訂單對象添加一個引用類型的屬性,這樣實現克隆事後,才能看出深度克隆的效果來。

那就定義一個產品對象,也須要讓它實現克隆的功能,產品對象實現的是一個淺度克隆。先來定義產品的原型接口,示例代碼以下:

/**
 * 聲明一個克隆產品自身的接口
 */
public interface ProductPrototype {
    /**
     * 克隆產品自身的方法
     * @return 一個從自身克隆出來的產品對象
     */
    public ProductPrototype cloneProduct();
}

接下來看看具體的產品對象實現,示例代碼以下:

/**
 * 產品對象
 */
public class Product implements ProductPrototype{
    /**
     * 產品編號
     */
    private String productId;  
    /**
     * 產品名稱
     */
    private String name;
    public String getName() {
       return name;
    }
    public void setName(String name) {
       this.name = name;
    }
    public String getProductId() {
       return productId;
    }
    public void setProductId(String productId) {
       this.productId = productId;
    }
    public String toString(){
       return "產品編號="+this.productId+",產品名稱="+this.name;
    }
    public ProductPrototype cloneProduct() {
       //建立一個新的訂單,而後把本實例的數據複製過去
       Product product = new Product();
       product.setProductId(this.productId);
       product.setName(this.name);    
       return product;
    }
}

(2)訂單的具體實現上也須要改變一下,須要在其屬性上添加一個產品類型的屬性,而後也須要實現克隆方法,示例代碼以下:

public class PersonalOrder implements OrderApi{
    private String customerName;
    private int orderProductNum = 0;
    /**
     * 產品對象
     */
    private Product product = null;
    public int getOrderProductNum() {
       return this.orderProductNum;
    }  
    public void setOrderProductNum(int num) {
       this.orderProductNum = num;
    }  
    public String getCustomerName() {
       return customerName;
    }
    public void setCustomerName(String customerName) {
       this.customerName = customerName;
    }
    public Product getProduct() {
       return product;
    }
    public void setProduct(Product product) {
       this.product = product;
    }  
    public String toString(){
       //簡單點輸出
       return "訂購產品是="+this.product.getName()+",訂購數量爲="+this.orderProductNum;
    }
    public OrderApi cloneOrder() {
       //建立一個新的訂單,而後把本實例的數據複製過去
       PersonalOrder order = new PersonalOrder();
       order.setCustomerName(this.customerName);
       order.setOrderProductNum(this.orderProductNum);
       //對於對象類型的數據,深度克隆的時候須要繼續調用這個對象的克隆方法
       order.setProduct((Product)this.product.cloneProduct());
       return order;
    }
}

(3)寫個客戶端來測試看看,是否深度克隆成功,示例代碼以下:

public class Client {
    public static void main(String[] args) {
       //先建立原型實例
       PersonalOrder oa1 = new PersonalOrder();
       //設置原型實例的值
       Product product = new Product();
       product.setName("產品1");
       oa1.setProduct(product);
       oa1.setOrderProductNum(100);

       System.out.println("這是第一次獲取的對象實例="+oa1);

       //經過克隆來獲取新的實例
       PersonalOrder oa2 = (PersonalOrder)oa1.cloneOrder();
       //修改它的值
       oa2.getProduct().setName("產品2");
       oa2.setOrderProductNum(80);
       //輸出克隆出來的對象的值
       System.out.println("輸出克隆出來的實例="+oa2);

       //再次輸出原型實例的值
       System.out.println("再次輸出原型實例="+oa1);
    }
}

(4)運行結果以下,很明顯,咱們本身作的深度克隆是成功的:

這是第一次獲取的對象實例=訂購產品是=產品1,訂購數量爲=100
輸出克隆出來的實例=訂購產品是=產品2,訂購數量爲=80
再次輸出原型實例=訂購產品是=產品1,訂購數量爲=100

(5)小結

看來本身實現深度克隆也不是很複雜,可是比較麻煩,若是產品類裏面又有屬性是引用類型的話,在產品類實現克隆方法的時候,又須要調用那個引用類型的克隆方法了,這樣一層一層調下去,若是中途有任何一個對象沒有正確實現深度克隆,那將會引發錯誤,這也是深度克隆容易出錯的緣由。

  1. Java中的深度克隆

(1)產品類沒有太大的不一樣,主要是把實現的接口變成了Cloneable,這樣一來,實現克隆的方法就不是cloneProduct,而是變成clone方法了;另一個是克隆方法的實現變成了使用「super.clone();」了,示例代碼以下:

public class Product implements Cloneable{
    private String productId;  
    private String name;
    public String getName() {
       return name;
    }
    public void setName(String name) {
       this.name = name;
    }
    public String getProductId() {
       return productId;
    }
    public void setProductId(String productId) {
       this.productId = productId;
    }
    public String toString(){
       return "產品編號="+this.productId+",產品名稱="+this.name;
    }
    public Object clone() {
       Object obj = null;
       try {
           obj = super.clone();
       } catch (CloneNotSupportedException e) {
           e.printStackTrace();
       }
       return obj;
    }  
}

(2)具體的訂單實現類,除了改變接口外,更重要的是在實現clone方法的時候,除了調用「super.clone();」外,必須顯示的調用引用類型屬性的clone方法,也就是產品的clone方法,示例代碼以下:

public class PersonalOrder implements Cloneable , OrderApi{
    private String customerName;
    private Product product = null;
    private int orderProductNum = 0;  
    public int getOrderProductNum() {
       return this.orderProductNum;
    }  
    public void setOrderProductNum(int num) {
       this.orderProductNum = num;
    }  
    public String getCustomerName() {
       return customerName;
    }
    public void setCustomerName(String customerName) {
       this.customerName = customerName;
    }
    public Product getProduct() {
       return product;
    }
    public void setProduct(Product product) {
       this.product = product;
    }  
    public String toString(){
       //簡單點輸出
       return "訂購產品是="+this.product.getName()+",訂購數量爲="+this.orderProductNum;
    }
    public Object clone(){
       PersonalOrder obj=null;
       try {
          obj =(PersonalOrder)super.clone();
          //下面這一句話不可少
          obj.setProduct((Product)this.product.clone());
       } catch (CloneNotSupportedException e) {
          e.printStackTrace();
       }     
       return obj;
    }
}

(3)特別強調:不可缺乏「obj.setProduct((Product)this.product.clone());」這句話。爲何呢?

緣由在於調用super.clone()方法的時候,Java是先開闢一塊內存的空間,而後把實例對象的值原樣拷貝過去,對於基本數據類型這樣作是沒有問題的,而屬性product是一個引用類型,把值拷貝過去的意思就是把對應的內存地址拷貝過去了,也就是說克隆後的對象實例的product和原型對象實例的product指向的是同一塊內存空間,是同一個產品實例

所以要想正確的執行深度拷貝,必須手工的對每個引用類型的屬性進行克隆,並從新設置,覆蓋掉super.clone()所拷貝的值

##3.4 原型管理器## 若是一個系統中原型的數目不固定,好比系統中的原型能夠被動態的建立和銷燬,那麼就須要在系統中維護一個當前可用的原型的註冊表,這個註冊表就被稱爲原型管理器。

其實若是把原型當成一個資源的話,原型管理器就至關於一個資源管理器,在系統開始運行的時候初始化,而後運行期間能夠動態的添加資源和銷燬資源。從這個角度看,原型管理器就能夠至關於一個緩存資源的實現,只不過裏面緩存和管理的是原型實例而已。

有了原型管理器事後,通常狀況下,除了向原型管理器裏面添加原型對象的時候是經過new來創造的對象,其他時候都是經過向原型管理器來請求原型實例,而後經過克隆方法來獲取新的對象實例,這就能夠實現動態管理、或者動態切換具體的實現對象實例。

仍是經過示例來講明,如何實現原型管理器。

  1. 先定義原型的接口,很是簡單,除了克隆方法,提供一個名稱的屬性,示例代碼以下:
public interface Prototype {
    public Prototype clone();
    public String getName();
    public void setName(String name);
}
  1. 再來看看兩個具體的實現,實現方式基本上是同樣的,分別看看。先看第一個原型的實現,示例代碼以下:
public class ConcretePrototype1 implements Prototype {
    private String name;
    public String getName() {
       return name;
    }
    public void setName(String name) {
       this.name = name;
    }
    public Prototype clone() {
       ConcretePrototype1 prototype = new ConcretePrototype1();
       prototype.setName(this.name);
       return prototype;
    }
    public String toString(){
       return "Now in Prototype1,name="+name;
    }
}

再看看第二個原型的實現,示例代碼以下:

public class ConcretePrototype2 implements Prototype {
    private String name;
    public String getName() {
       return name;
    }
    public void setName(String name) {
       this.name = name;
    }
    public Prototype clone() {
       ConcretePrototype2 prototype = new ConcretePrototype2();
       prototype.setName(this.name);
       return prototype;
    }  
    public String toString(){
       return "Now in Prototype2,name="+name;
    }
}
  1. 接下來看看原型管理器的實現示意,示例代碼以下:
/**
 * 原型管理器
 */
public class PrototypeManager {
    /**
     * 用來記錄原型的編號和原型實例的對應關係
     */
    private static Map<String,Prototype> map = new HashMap<String,Prototype>();
    /**
     * 私有化構造方法,避免外部無謂的建立實例
     */
    private PrototypeManager(){
       //
    }
    /**
     * 向原型管理器裏面添加或是修改某個原型註冊
     * @param prototypeId 原型編號
     * @param prototype 原型實例
     */
    public synchronized static void setPrototype(String prototypeId,Prototype prototype){
       map.put(prototypeId, prototype);
    }
    /**
     * 從原型管理器裏面刪除某個原型註冊
     * @param prototypeId 原型編號
     */
    public synchronized static void removePrototype(String prototypeId){
       map.remove(prototypeId);
    }
    /**
     * 獲取某個原型編號對應的原型實例
     * @param prototypeId 原型編號
     * @return 原型編號對應的原型實例
     * @throws Exception 若是原型編號對應的原型實例不存在,報出例外
     */
    public synchronized static Prototype getPrototype(String prototypeId)throws Exception{
       Prototype prototype = map.get(prototypeId);
       if(prototype == null){
           throw new Exception("您但願獲取的原型尚未註冊或已被銷燬");
       }
       return prototype;
    }
}

你們會發現,原型管理器是相似一個工具類的實現方式,並且對外的幾個方法都是加了同步的,這主要是由於若是在多線程環境下使用這個原型管理器的話,那個map屬性很明顯就成了你們競爭的資源,所以須要加上同步。

  1. 接下來看看客戶端,如何使用這個原型管理器,示例代碼以下:
public class Client {
    public static void main(String[] args) {
       try {
           // 初始化原型管理器
           Prototype p1 = new ConcretePrototype1();
           PrototypeManager.setPrototype("Prototype1", p1);

           // 獲取原型來建立對象
           Prototype p3 = PrototypeManager.getPrototype("Prototype1").clone();
           p3.setName("張三");
           System.out.println("第一個實例:" + p3);

           // 有人動態的切換了實現
           Prototype p2 = new ConcretePrototype2();
           PrototypeManager.setPrototype("Prototype1", p2);

           // 從新獲取原型來建立對象
           Prototype p4 = PrototypeManager.getPrototype("Prototype1").clone();
           p4.setName("李四");
           System.out.println("第二個實例:" + p4);

           // 有人註銷了這個原型
           PrototypeManager.removePrototype("Prototype1");

           // 再次獲取原型來建立對象
           Prototype p5 = PrototypeManager.getPrototype("Prototype1").clone();
           p5.setName("王五");
           System.out.println("第三個實例:" + p5);
       } catch (Exception err) {
           System.err.println(err.getMessage());
       }
    }
}

運行一下,看看結果,結果示例以下:

第一個實例:Now in Prototype1,name=張三
第二個實例:Now in Prototype2,name=李四
您但願獲取的原型尚未註冊或已被銷燬

##3.5 原型模式的優缺點##

  1. 對客戶端隱藏具體的實現類型

原型模式的客戶端,只知道原型接口的類型,並不知道具體的實現類型,從而減小了客戶端對這些具體實現類型的依賴。

  1. 在運行時動態改變具體的實現類型

原型模式能夠在運行期間,由客戶來註冊符合原型接口的實現類型,也能夠動態的改變具體的實現類型,看起來接口沒有任何變化,但其實運行的已是另一個類實例了。由於克隆一個原型就相似於實例化一個類。

  1. 深度克隆方法實現會比較困難

原型模式最大的缺點就在於每一個原型的子類都必須實現clone的操做,尤爲在包含引用類型的對象時,clone方法會比較麻煩,必需要可以遞歸的讓全部的相關對象都要正確的實現克隆。

##3.6 思考原型模式##

  1. 原型模式的本質

原型模式的本質:克隆生成對象。

克隆是手段,目的仍是生成新的對象實例。正是由於原型的目的是爲了生成新的對象實例,原型模式一般是被歸類爲建立型的模式

原型模式也能夠用來解決「只知接口而不知實現的問題」,使用原型模式,能夠出現一種獨特的「接口造接口」的景象,這在面向接口編程中頗有用。一樣的功能也能夠考慮使用工廠來實現。

另外,原型模式的重心仍是在建立新的對象實例,至於建立出來的對象,其屬性的值是否必定要和原型對象屬性的值徹底同樣,這個並無強制規定,只不過在目前大多數實現中,克隆出來的對象和原型對象的屬性值是同樣的。

也就是說,能夠經過克隆來創造值不同的實例,可是對象類型必須同樣。能夠有部分甚至是所有的屬性的值不同,能夠有選擇性的克隆,就當是標準原型模式的一個變形使用吧。

  1. 什麼時候選用原型模式

建議在以下狀況中,選用原型模式:

若是一個系統想要獨立於它想要使用的對象時,可使用原型模式,讓系統只面向接口編程,在系統須要新的對象的時候,能夠經過克隆原型來獲得;

若是須要實例化的類是在運行時刻動態指定時,可使用原型模式,經過克隆原型來獲得須要的實例;

##3.7 相關模式##

  1. 原型模式和抽象工廠模式

功能上有些類似,都是用來獲取一個新的對象實例的。

不一樣之處在於,原型模式的着眼點是在如何創造出實例對象來,最後選擇的方案是經過克隆;而抽象工廠模式的着眼點則在於如何來創造產品簇,至於具體如何建立出產品簇中的每一個對象實例,抽象工廠模式不是很關注。

正是由於它們的關注點不同,因此它們也能夠配合使用,好比在抽象工廠模式裏面,具體建立每一種產品的時候就可使用該種產品的原型,也就是抽象工廠管產品簇,具體的每種產品怎麼建立則能夠選擇原型模式。

  1. 原型模式和生成器模式

這兩種模式能夠配合使用。

生成器模式關注的是構建的過程,而在構建的過程當中,極可能須要某個部件的實例,那麼很天然地就能夠應用上原型模式,經過原型模式來獲得部件的實例。

相關文章
相關標籤/搜索