橘子学设计模式之原型模式
一、什么是原型模式
關于什么是原型模式,我們看一個例子先。
我們都知道華夏民族的子孫都是女媧拿泥捏出來的,我們站在偉大人文始祖女媧的角度來思考一下這個過程。
剛開始女媧大佬精力旺盛,上來就是手捏你人泥人,精雕細琢出一個個帥哥美女。但是大佬也會累,你每次都從新開始捏一個泥人,真的很累。于是大佬想了一個新的方法,創造了一個模子,每次先從模子里面復刻出來一個泥人,也許此時泥人還不是最后的結果,但是大佬此時就能在這個復刻出來的模子上面做一個簡單的處理就能創建出一個新的泥人,想想這種操作也是極其的節省時間的。
其實這就是一個原型模式的體現。
# 我們這里來給原型模式下一個比較書面話的定義: 原型模式的工作原理很簡單:將一個原型對象傳給那個要發動創建的對象,這個要發動創建的對象通過請求原型對象拷貝自己來實現創建過程。由于在軟件系統中我們經常會遇到需要創建多個相同或者相似對象的情況,因此原型模式在真實開發中的使用頻率還是非常高的。 原型模式是一種“另類”的創建型模式,創建克隆對象的工廠就是原型類自身(自身提供出一個創建的方法,調用創建出對應的克隆對象),工廠方法由克隆方法來實現。 需要注意的是通過克隆方法所創建的對象是全新的對象,它們在內存中擁有新的地址,通常 對克隆所產生的對象進行修改對原型對象不會造成任何影響,每一個克隆對象都是相互獨立的。通過不同的方式修改可以得到一系列相似但不完全相同的對象。所有我們看到原型模式其實和女媧造人的例子極其的相似。而且通過例子和定義,我們基本能提煉出這個原型模式的一些角色組成。
- Prototype(抽象原型類):它是聲明克隆方法的接口,是所有具體原型類的公共父類,可以 是抽象類也可以是接口,甚至還可以是具體實現類。
- ConcretePrototype(具體原型類):它實現在抽象原型類中聲明的克隆方法,在克隆方法中 返回自己的一個克隆對象。
- Client(客戶類):讓一個原型對象克隆自身從而創建一個新的對象,在客戶類中只需要直 接實例化或通過工廠方法等方式創建一個原型對象,再通過調用該對象的克隆方法即可得到 多個相同的對象。由于客戶類針對抽象原型類Prototype編程,因此用戶可以根據需要選擇具體 原型類,系統具有較好的可擴展性,增加或更換具體原型類都很方便。
所以我們看到原型模式的核心在于如何實現克隆方法,因為你此模式本身就是干這個的,所以這是他的核心工作。一般實現有兩種方式組成。
二、原型模式的實現方式
1、通用創建
所謂通用創建其實就是不依賴jdk提供的api,說白了就是自己實現了,不調用方法。
通用的克隆實現方法是在具體原型類的克隆方法中實例化一個與自身類型相同的對象并將其 返回,并將相關的參數傳入新創建的對象中,保證它們的成員屬性相同。示意代碼如下所 示:
頂級抽象類:
package com.liuwei.prototype.test1;/** * @Author author * @Description 接口定義 * @Date 19:05 2022-4-3 * @Param * @param null * @return **/ public interface Prototype { public String attr = ""; //成員屬性 public Prototype clone(); public void setAttr(String attr); public String getAttr(); }實現類:這是原型類,提供clone方法,其實就是自己new出來
package com.liuwei.prototype.test1;/** * @author: author * @description: 原型類,實現clone方法 * @date: 2022-4-3 19:06 * @version: 1.0 */ public class ConcretePrototype implements Prototype{ private String attr; //成員屬性 public void setAttr(String attr) { this.attr = attr; } public String getAttr() { return this.attr; } //克隆方法 public Prototype clone() { //創建新對象 Prototype prototype = new ConcretePrototype(); prototype.setAttr(this.attr); return prototype; } }客戶端:
package com.liuwei.prototype.test1;/** * @author: author * @description: 客戶端 * @date: 2022-4-3 19:10 * @version: 1.0 */ public class Client { public static void main(String[] args) { Prototype obj1 = new ConcretePrototype(); obj1.setAttr("SB"); Prototype obj2 = obj1.clone(); System.out.println(obj1); System.out.println(obj2); } }>>>>>>>>>>>輸出結果com.liuwei.prototype.test1.ConcretePrototype@29453f44com.liuwei.prototype.test1.ConcretePrototype@5cad8086這種方法可作為原型模式的通用實現,它與編程語言特性無關,任何面向對象語言都可以使 用這種形式來實現對原型的克隆。 因為沒有使用的說固定的誰提供的API,所以其實不依賴于語言類型。
2、Java語言提供的clone()方法
學過Java語言的人都知道,所有的Java類都繼承自java.lang.Object。事實上,Object類提供一個 clone()方法,可以將一個Java對象復制一份。因此在Java中可以直接使用Object提供的clone()方 法來實現對象的克隆,Java語言中的原型模式實現很簡單。
需要注意的是能夠實現克隆的Java類必須實現一個標識接口Cloneable,表示這個Java類支持被復制。如果一個類沒有實現這個接口但是調用了clone()方法,Java編譯器將拋出一個 CloneNotSupportedException異常。如下代碼所示:
package com.liuwei.prototype.test2; import com.liuwei.prototype.Prototype;/** * @author: author * @description: TODO * @date: 2022-4-3 19:18 * @version: 1.0 */ class ConcretePrototype implements Cloneable { //自己實現Cloneable接口的clone方法 public Prototype clone() { Object object = null; try { // 直接調用Object類提供的克隆方法即可 object = super.clone(); } catch (CloneNotSupportedException exception) { System.err.println("Not support cloneable"); } return (Prototype ) object; }}在客戶端創建原型對象和克隆對象也很簡單,如下代碼所示:
Prototype obj1 = new ConcretePrototype();
Prototype obj2 = obj1.clone();
# 一般而言,Java語言中的clone()方法滿足: (1) 對任何對象x,都有x.clone() != x,即克隆對象與原型對象不是同一個對象; (2) 對任何對象x,都有x.clone().getClass() == x.getClass(),即克隆對象與原型對象的類型一 樣; (3) 如果對象x的equals()方法定義恰當,那么x.clone().equals(x)應該成立。# 為了獲取對象的一份拷貝,我們可以直接利用Object類的clone()方法,具體步驟如下: (1) 在派生類中覆蓋基類的clone()方法,并聲明為public; (2) 在派生類的clone()方法中,調用super.clone(); (3)派生類需實現Cloneable接口。 此時,Object類相當于抽象原型類,所有實現了Cloneable接口的類相當于具體原型類。三、原型模式的使用
1、前情提要
SB軟件公司一直使用自行開發的一套OA (Office Automatic,辦公自動化)系統進行日常工作 辦理,但在使用過程中,越來越多的人對工作周報的創建和編寫模塊產生了抱怨。追其原 因,SB軟件公司的OA管理員發現,由于某些崗位每周工作存在重復性,工作周報內容都 大同小異,如圖7-1工作周報示意圖。這些周報只有一些小地方存在差異,但是現行系統每周 默認創建的周報都是空白報表,用戶只能通過重新輸入或不斷復制粘貼來填寫重復的周報內 容,極大降低了工作效率,浪費寶貴的時間。如何快速創建相同或者相似的工作周報,成為 SB公司OA開發人員面臨的一個新問題。 SB公司的開發人員通過對問題進行仔細分析,決定按照如下思路對工作周報模塊進行重新 設計和實現: (1)除了允許用戶創建新周報外,還允許用戶將創建好的周報保存為模板; (2)用戶在再次創建周報時,可以創建全新的周報,還可以選擇合適的模板復制生成一份相同 的周報,然后對新生成的周報根據實際情況進行修改,產生新的周報。2、原型模式上手
類圖結構如下:
WeeklyLog充當具體原型類,Object類充當抽象原型類,clone()方法為原型方法。 WeeklyLog類的代碼如下所示:
原型類:
package com.liuwei.prototype.test3;/*** @author: author* @description: TODO* @date: 2022-4-3 19:31* @version: 1.0*/ public class WeeklyLog implements Cloneable{private String name;private String date;private String content;//克隆方法clone(),此處使用Java語言提供的克隆機制public WeeklyLog clone() {Object obj = null;try {obj = super.clone();return (WeeklyLog)obj;}catch(CloneNotSupportedException e) {System.out.println("不支持復制!");return null;}}get set省略。 }客戶端:
package com.liuwei.prototype.test3; /** * @author: author * @description: TODO * @date: 2022-4-3 19:33 * @version: 1.0 */ public class Client { public static void main(String args[]) { WeeklyLog log_previous = new WeeklyLog(); //創建原型對象 log_previous.setName("劉德華"); log_previous.setDate("第12周"); log_previous.setContent("這周工作很忙,每天加班!"); System.out.println("****周報****"); System.out.println("周次:" + log_previous.getDate()); System.out.println("姓名:" + log_previous.getName()); System.out.println("內容:" + log_previous.getContent()); System.out.println("--------------------------------"); WeeklyLog log_new; log_new = log_previous.clone(); //調用克隆方法創建克隆對象 log_new.setDate("第13周"); System.out.println("****周報****"); System.out.println("周次:" + log_new.getDate()); System.out.println("姓名:" + log_new.getName()); System.out.println("內容:" + log_new.getContent()); } }通過已創建的工作周報可以快速創建新的周報,然后再根據需要修改周報,無須再從頭開始 創建。原型模式為工作流系統中任務單的快速生成提供了一種解決方案。
3、深拷貝和淺拷貝
通過引入原型模式,SB軟件公司OA系統支持工作周報的快速克隆,極大提高了工作周報 的編寫效率,受到員工的一致好評。但有員工又發現一個問題,有些工作周報帶有附件,例如經理助理“小龍女”的周報通常附有本周項目進展報告匯總表、本周客戶反饋信息匯總表等, 如果使用上述原型模式來復制周報,周報雖然可以復制,但是周報的附件并不能復制,這是由于什么原因導致的呢?如何才能實現周報和附件的同時復制呢? 我們在本節將討論如何解決這些問題。 在回答這些問題之前,先介紹一下兩種不同的克隆方法,淺克隆(ShallowClone)和深克隆 (DeepClone)。在Java語言中,數據類型分為值類型(基本數據類型)和引用類型,值類型包括 int、double、byte、boolean、char等簡單數據類型,引用類型包括類、接口、數組等復雜類型。淺克隆和深克隆的主要區別在于是否支持引用類型的成員變量的復制,下面將對兩者進行詳細介紹。其意思就是我以前都是基本類型屬性,所以能拷貝。但是要是有對象這種屬性,那就復制不出來了,這就是涉及到深淺的拷貝。3.1、淺克隆
在淺克隆中,如果原型對象的成員變量是值類型,將復制一份給克隆對象;如果原型對象的 成員變量是引用類型,則將引用對象的地址復制一份給克隆對象,也就是說原型對象和克隆 對象的成員變量指向相同的內存地址。簡單來說,在淺克隆中,當對象被復制時只復制它本身和其中包含的值類型的成員變量,而引用類型的成員對象并沒有復制,淺克隆示意圖如圖下所示:
在Java語言中,通過覆蓋Object類的clone()方法可以實現淺克隆。為了讓大家更好地理解淺克 隆和深克隆的區別,我們首先使用淺克隆來實現工作周報和附件類的復制,其結構如下圖:
帶附件的周報結構圖(淺克隆):
附件類Attachment代碼如下:
package com.liuwei.prototype.test4;import lombok.Data;/*** @author: author* @description: 附件類* @date: 2022-4-3 20:52* @version: 1.0*/ @Data public class Attachment {private String name; //附件名public void download() {System.out.println("下載附件,文件名為" + name);} }工作周報WeeklyLog類:
package com.liuwei.prototype.test4;import lombok.Data;/*** @author: author* @description: 工作周報WeeklyLog* @date: 2022-4-3 20:53* @version: 1.0*/ @Data public class WeeklyLog implements Cloneable{//為了簡化設計和實現,假設一份工作周報中只有一個附件對象,實際情況中可以包含多個附件,可以通過List等集合對象來實現private Attachment attachment;private String name;private String date;private String content;//使用clone()方法實現淺克隆public WeeklyLog clone() {Object obj = null;try {obj = super.clone();return (WeeklyLog)obj;}catch(CloneNotSupportedException e) {System.out.println("不支持復制!");return null;}} }客戶端:
package com.liuwei.prototype.test4;/*** @author: author* @description: 客戶端代碼如下所示:* @date: 2022-4-3 20:55* @version: 1.0*/ public class Client {public static void main(String[] args) {WeeklyLog log_previous, log_new;log_previous = new WeeklyLog(); //創建原型對象Attachment attachment = new Attachment(); //創建附件對象log_previous.setAttachment(attachment); //將附件添加到周報中log_new = log_previous.clone(); //調用克隆方法創建克隆對象// 比較周報System.out.println("周報是否相同? " + (log_previous == log_new));//比較附件System.out.println("附件是否相同? " + (log_previous.getAttachment() == attachment));} }>>>>>>>>>>>>運行結果 周報是否相同? false 附件是否相同? true 由于使用的是默認的淺克隆技術,因此工作周報對象復制成功,通過“==”比較原型對象和克隆對象 的內存地址時輸出false;但是比較附件對象的內存地址時輸出true,說明它們在內存中是同一個對象。3.2、深克隆
在深克隆中,無論原型對象的成員變量是值類型還是引用類型,都將復制一份給克隆對象, 深克隆將原型對象的所有引用對象也復制一份給克隆對象。簡單來說,在深克隆中,除了對象本身被復制外,對象所包含的所有成員變量也將復制,深克隆示意如下圖所示:
在Java語言中,如果需要實現深克隆,可以通過序列化(Serialization)等方式來實現。序列化就 是將對象寫到流的過程,寫到流中的對象是原有對象的一個拷貝,而原對象仍然存在于內存 中。通過序列化實現的拷貝不僅可以復制對象本身,而且可以復制其引用的成員對象,因此 通過序列化將對象寫到一個流中,再從流里將其讀出來,可以實現深克隆。需要注意的是能 夠實現序列化的對象其類必須實現Serializable接口,否則無法實現序列化操作。下面我們使用 深克隆技術來實現工作周報和附件對象的復制,由于要將附件對象和工作周報對象都寫入流 中,因此兩個類均需要實現Serializable接口,帶附件的周報結構圖(深克隆)如下圖所示:
附件類Attachment:
package com.liuwei.prototype.test5;import lombok.Data;import java.io.Serializable;/*** @author: author* @description: 附件類* @date: 2022-4-3 21:05* @version: 1.0*/ @Data public class Attachment implements Serializable {private String name; //附件名public void download() {System.out.println("下載附件,文件名為" + name);} }工作周報類WeeklyLog不再使用Java自帶的克隆機制,而是通過序列化來從頭實現對象的深克 隆,我們需要重新編寫clone()方法,修改后的代碼如下:
工作周報類:
package com.liuwei.prototype.test5;import lombok.Data;import java.io.*;/*** @author: author* @description: 工作周報類* @date: 2022-4-3 21:07* @version: 1.0*/ @Data public class WeeklyLog implements Serializable {private Attachment attachment;private String name;private String date;private String content;//使用序列化技術實現深克隆public WeeklyLog deepClone() throws IOException, ClassNotFoundException, OptionalDataException {//將對象寫入流中ByteArrayOutputStream bao=new ByteArrayOutputStream();ObjectOutputStream oos=new ObjectOutputStream(bao);oos.writeObject(this);//將對象從流中取出ByteArrayInputStream bis=new ByteArrayInputStream(bao.toByteArray());ObjectInputStream ois=new ObjectInputStream(bis);return (WeeklyLog)ois.readObject();} }客戶端:
package com.liuwei.prototype.test5;/*** @author: author* @description: 客戶端代碼* @date: 2022-4-3 21:10* @version: 1.0*/ public class Client {public static void main(String[] args) {WeeklyLog log_previous, log_new = null;log_previous = new WeeklyLog(); //創建原型對象Attachment attachment = new Attachment(); //創建附件對象log_previous.setAttachment(attachment); //將附件添加到周報try {log_new = log_previous.deepClone(); //調用深克隆}catch(Exception e) {System.err.println("克隆失敗!");}//比較周報System.out.println("周報是否相同? " + (log_previous == log_new));//比較附件System.out.println("附件是否相同? " + (log_previous.getAttachment() == log_new.getAttachment()));} } >>>>>>>>>>>>>運行結果 周報是否相同? false 附件是否相同? false 從輸出結果可以看出,由于使用了深克隆技術,附件對象也得以復制,因此用“==”比較原型 對象的附件和克隆對象的附件時輸出結果均為false。深克隆技術實現了原型對象和克隆對象的完全獨立,對任意克隆對象的修改都不會給其他對象產生影響,是一種更為理想的克隆實現方式。Java語言提供的Cloneable接口和Serializable接口的代碼非常簡單,它們都是空接口,這種空接口也稱為標識接口,標識接口中沒有任何方法的定義,其作用是告訴JRE這些接口的實現類是否具有某個功能,如是否支持克隆、是否支持序列化等。
4、終極原型模式
4.1、原型管理器的引入和實現
這里其實就是一個工廠類對于原型的一些封裝管理,其余的其實不必過多理解。
原型管理器(Prototype Manager)是將多個原型對象存儲在一個集合中供客戶端使用,它是一個專門負責克隆對象的工廠,其中定義了一個集合用于存儲原型對象,如果需要某個原型對象的一個克隆,可以通過復制集合中對應的原型對象來獲得。在原型管理器中針對抽象原型類進行編程,以便擴展。帶原型管理器的原型模式,其結構如下圖所示:
下面通過模擬一個簡單的公文管理器來介紹原型管理器的設計與實現: SB軟件公司在日常 辦公中有許多公文需要創建、遞交和審批,例如《可行性分析報告》、《立項建議書》、 《軟件需求規格說明書》、《項目進展報告》等,為了提高工作效率,在OA系統中為各類公文均創建了模板,用戶可以通過這些模板快速創建新的公文,這些公文模板需要統一進行管理,系統根據用戶請求的不同生成不同的新公文。
我們使用帶原型管理器的原型模式實現公文管理器的設計, 公文管理器結構圖如下圖所示:
以下是實現該功能的一些核心代碼,考慮到代碼的可讀性,我們對所有的類都進行了簡化:
抽象公文接口:
package com.liuwei.prototype.test6;/*** @author: author* @description: 抽象公文接口,也可定義為抽象類,提供clone()方法的實現,將業務方法聲明為抽象方法* @date: 2022-4-3 21:22* @version: 1.0*/ public interface OfficialDocument extends Cloneable{public OfficialDocument clone();public void display(); }可行性分析報告(Feasibility Analysis Report)類
package com.liuwei.prototype.test6;/*** @author: author* @description: 可行性分析報告(Feasibility Analysis Report)類,一種策略實現* @date: 2022-4-3 21:24* @version: 1.0*/ public class FAR implements OfficialDocument{@Overridepublic OfficialDocument clone() {OfficialDocument far = null;try {far = (OfficialDocument)super.clone();}catch(CloneNotSupportedException e) {System.out.println("不支持復制!");}return far;}@Overridepublic void display() {System.out.println("《可行性分析報告》");} }軟件需求規格說明書(Software Requirements Specification)類
package com.liuwei.prototype.test6;/*** @author: author* @description: 軟件需求規格說明書(Software Requirements Specification)類* @date: 2022-4-3 21:25* @version: 1.0*/ class SRS implements OfficialDocument {public OfficialDocument clone() {OfficialDocument srs = null;try {srs = (OfficialDocument)super.clone();}catch(CloneNotSupportedException e) {System.out.println("不支持復制!");}return srs;}public void display() {System.out.println("《軟件需求規格說明書》");} }原型管理器(使用餓漢式單例實現):
package com.liuwei.prototype.test6;import java.util.Hashtable;/*** @author: author* @description: 原型管理器(使用餓漢式單例實現),這里類似一個工廠類了* @date: 2022-4-3 21:26* @version: 1.0*/ public class PrototypeManager {//定義一個Hashtable,用于存儲原型對象private Hashtable ht=new Hashtable();//初始化實例對象,這里new的時候就把公文對象塞進去private static PrototypeManager pm = new PrototypeManager();//為Hashtable增加公文對象,構造初始化的時候添加對象類private PrototypeManager() {ht.put("far",new FAR());ht.put("srs",new SRS());}//增加新的公文對象public void addOfficialDocument(String key,OfficialDocument doc) {ht.put(key,doc);}//通過淺克隆獲取新的公文對象public OfficialDocument getOfficialDocument(String key) {return ((OfficialDocument)ht.get(key)).clone();}//get方法public static PrototypeManager getPrototypeManager(){return pm;} }客戶端:
package com.liuwei.prototype.test6;/*** @author: author* @description: 客戶端* @date: 2022-4-3 21:28* @version: 1.0*/ public class Client {public static void main(String[] args) {//獲取原型管理器對象PrototypeManager pm = PrototypeManager.getPrototypeManager();OfficialDocument doc1,doc2,doc3,doc4;doc1 = pm.getOfficialDocument("far");doc1.display();doc2 = pm.getOfficialDocument("far");doc2.display();System.out.println(doc1 == doc2);doc3 = pm.getOfficialDocument("srs");doc3.display();doc4 = pm.getOfficialDocument("srs");doc4.display();System.out.println(doc3 == doc4);} } >>>>>>>>>>運行結果 《可行性分析報告》 《可行性分析報告》 false 《軟件需求規格說明書》 《軟件需求規格說明書》 false # 原理:其實就是引入了個工廠 在PrototypeManager中定義了一個Hashtable類型的集合對象,使用“鍵值對”來存儲原型對象, 客戶端可以通過Key(如“far”或“srs”)來獲取對應原型對象的克隆對象。PrototypeManager類 提供了類似工廠方法的getOfficialDocument()方法用于返回一個克隆對象。在本實例代碼中, 我們將PrototypeManager設計為單例類,使用餓漢式單例實現,確保系統中有且僅有一個 PrototypeManager對象,有利于節省系統資源,并可以更好地對原型管理器對象進行控制。四、原型模式總結
原型模式作為一種快速創建大量相同或相似對象的方式,在軟件開發中應用較為廣泛,很多軟件提供的復制(Ctrl + C)和粘貼(Ctrl + V)操作就是原型模式的典型應用,下面對該模式的使用效果和適用情況進行簡單的總結。
# 1.主要優點 原型模式的主要優點如下: (1) 當創建新的對象實例較為復雜時,使用原型模式可以簡化對象的創建過程,通過復制一個已有實例可以提高新實例的創建效率。 (2) 擴展性較好,由于在原型模式中提供了抽象原型類,在客戶端可以針對抽象原型類進行編 程,而將具體原型類寫在配置文件中,增加或減少產品類對原有系統都沒有任何影響。 (3) 原型模式提供了簡化的創建結構,工廠方法模式常常需要有一個與產品類等級結構相同的工廠等級結構,而原型模式就不需要這樣,原型模式中產品的復制是通過封裝在原型類中的 克隆方法實現的,無須專門的工廠類來創建產品。 (4) 可以使用深克隆的方式保存對象的狀態,使用原型模式將對象復制一份并將其狀態保存起來,以便在需要的時候使用(如恢復到某一歷史狀態),可輔助實現撤銷操作。 # 2.主要缺點 原型模式的主要缺點如下: (1) 需要為每一個類配備一個克隆方法,而且該克隆方法位于一個類的內部,當對已有的類進行改造時,需要修改源代碼,違背了“開閉原則”。 (2) 在實現深克隆時需要編寫較為復雜的代碼,而且當對象之間存在多重的嵌套引用時,為了實現深克隆,每一層對象對應的類都必須支持深克隆,實現起來可能會比較麻煩。 # 3.適用場景 在以下情況下可以考慮使用原型模式: (1) 創建新對象成本較大(如初始化需要占用較長的時間,占用太多的CPU資源或網絡資源),新的對象可以通過原型模式對已有對象進行復制來獲得,如果是相似對象,則可以對 其成員變量稍作修改。 (2) 如果系統要保存對象的狀態,而對象的狀態變化很小,或者對象本身占用內存較少時,可以使用原型模式配合備忘錄模式來實現。 (3) 需要避免使用分層次的工廠類來創建分層次的對象,并且類的實例對象只有一個或很少的幾個組合狀態,通過復制原型對象得到新實例可能比使用構造函數創建一個新實例更加方便。總結
以上是生活随笔為你收集整理的橘子学设计模式之原型模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ubuntu中解/压缩命令
- 下一篇: Pyspark学习笔记1——配置环境并计