研磨設計模式 之 原型模式(Prototype)2 ——跟著cc學設計系列
9.2 解決方案
9.2.1 原型模式來解決
用來解決上述問題的一個合理的解決方案就是原型模式。那麼什麼是原型模式呢?
(1 )原型模式定義
(2 )應用原型模式來解決的思路
仔細分析上面的問題,在saveOrder方法裡面,已經有了訂單介面型別的物件例項,是從外部傳入的,但是這裡只是知道這個例項物件的種類是訂單的介面型別,並不知道其具體的實現型別,也就是不知道它到底是個人訂單還是企業訂單,但是現在需要在這個方法裡面建立一個這樣的訂單物件,看起來就像是要通過介面來建立物件一樣。
原型模式就可以解決這樣的問題,原型模式會要求物件實現一個可以“克隆”自身的介面,這樣就可以通過拷貝或者是克隆一個例項物件本身,來建立一個新的例項。如果把這個方法定義在介面上,看起來就像是通過介面來建立了新的介面物件。
這樣一來,通過原型例項建立新的物件,就不再需要關心這個例項本身的型別,也不關心它的具體實現,只要它實現了克隆自身的方法,就可以通過這個方法來獲取新的物件,而無須再去通過new來建立。
9.2.1 模式結構和說明
原型模式的結構如圖9.1所示:
圖9.1 原型模式結構示意圖
Prototype :
宣告一個克隆自身的介面,用來約束想要克隆自己的類,要求它們都要實現這裡定義的克隆方法。
ConcretePrototype :
實現Prototype介面的類,這些類真正實現了克隆自身的功能。
Client :
使用原型的客戶端,首先要獲取到原型例項物件,然後通過原型例項克隆自身來建立新的物件例項。
9.2.3 原型模式示例程式碼
(1)先來看看原型介面的定義,示例程式碼如下:
/** * 宣告一個克隆自身的介面 */ public interface Prototype { /** * 克隆自身的方法 * @return 一個從自身克隆出來的物件 */ public Prototype clone(); } |
(2)接下來看看具體的原型實現物件,示例程式碼如下:
/** * 克隆的具體實現物件 */ 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; } } |
為了跟上面原型模式的結構示意圖保持一致,因此這兩個具體的原型實現物件,都沒有定義屬性。事實上,在實際使用原型模式的應用中,原型物件多是有屬性的,克隆原型的時候也是需要克隆原型物件的屬性的,特此說明一下。
(3)再看看使用原型的客戶端,示例程式碼如下:
/** * 使用原型的客戶端 */ public class Client { /** * 持有需要使用的原型介面物件 */ private Prototype prototype; /** * 構造方法,傳入需要使用的原型介面物件 * @param prototype 需要使用的原型介面物件 */ public Client(Prototype prototype){ this.prototype = prototype; } /** * 示意方法,執行某個功能操作 */ public void operation(){ //會需要建立原型介面的物件 Prototype newPrototype = prototype.clone(); } } |
9.2.4 使用原型模式重寫示例
要使用原型模式來重寫示例,先要在訂單的介面上定義出克隆的介面,然後要求各個具體的訂單物件克隆自身,這樣就可以解決:在訂單處理物件裡面通過訂單介面來建立新的訂單物件的問題。
使用原型模式來重寫示例的結構如圖9.2所示:
圖9.2 使用原型模式來重寫示例的結構示意圖
下面一起來看看具體的實現。
(1 )複製誰和誰來複制的問題
有了一個物件例項,要快速的建立跟它一樣的例項,最簡單的辦法就是複製?這裡又有兩個小的問題
- 複製誰呢?當然是複製這個物件例項,複製例項的意思是連帶著資料一起復制。
- 誰來複制呢?應該讓這個類的例項自己來複制,自己複製自己。
可是每個物件不會那麼聽話,自己去實現複製自己的。於是原型模式決定對這些物件實行強制要求,給這些物件定義一個介面,在接口裡面定義一個方法,這個方法用來要求每個物件實現自己複製自己。
由於現在存在訂單的介面,因此就把這個要求克隆自身的方法定義在訂單的接口裡面,示例程式碼如下:
/** * 訂單的介面,聲明瞭可以克隆自身的方法 */ public interface OrderApi { public int getOrderProductNum(); public void setOrderProductNum(int num); /** * 克隆方法 * @return 訂單原型的例項 */ public OrderApi cloneOrder(); } |
(2 )如何克隆
定義好了克隆的介面,那麼在訂單的實現類裡面,就得讓它實現這個介面,並具體的實現這個克隆方法,新的問題出來了,如何實現克隆呢?
很簡單,只要先new一個自己物件的例項,然後把自己例項中的資料取出來,設定到新的物件例項中去,不就可以完成例項的複製了嘛,複製的結果就是有了一個跟自身一模一樣的例項。
有的朋友可能會說:不用那麼費勁吧,直接返回本例項不就可以了?如下:
public Object clone(){ return this; } |
請注意了,這是一種典型的錯誤 ,這麼做,每次克隆,客戶端獲取的其實都是同一個例項,都是指向同一個記憶體空間的,對克隆出來的物件例項的修改會影響到原型物件例項。
那麼應該怎麼克隆呢,最基本的做法就是新建一個類例項,然後把所有屬性的值複製到新的例項中,先看看個人訂單物件的實現,示例程式碼如下:
/** * 個人訂單物件 */ 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; } } |
(3 )使用克隆方法
這裡使用訂單介面的克隆方法的,是訂單的處理物件,也就是說,訂單的處理物件就相當於原型模式結構中的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); } } |
(4)客戶端的測試程式碼,跟前面的示例是完全一樣的,這裡就不去贅述了,去執行一下,看看執行的效果,享受一下克隆的樂趣。
在上面的例子中,在訂單處理物件的儲存訂單方法裡面的這句話“OrderApi newOrder = order.cloneOrder();”,就用一個訂單的原型例項來指定了物件的種類,然後通過克隆這個原型例項來創建出了一個新的物件例項。
9.2.5 小提示
看到這裡,可能有些朋友會認為:Java的Object裡面本身就有clone方法,還用搞得這麼麻煩嗎?
雖然Java裡面有clone方法,上面這麼做還是很有意義的,可以讓我們更好的、更完整的體會原型這個設計模式。當然,後面會講述如何使用Java裡面的clone方法來實現克隆,不要著急。