原型模式
原型模式介紹
定義: 原型模式(Prototype Design Pattern)用一個已經創建的實例作為原型,通過復制該原型對象來創建一個和原型對象相同的新對象。
西游記中的孫悟空,拔毛變小猴,孫悟空這種根據自己的形狀復制出多個身外化身的技巧,在面向對象軟件設計領域被稱為原型模式。孫悟空就是原型對象。
原型模式主要解決的問題
- 如果創建對象的成本比較大,比如對象中的數據是經過復雜計算才能得到,或者需要從RPC接口或者數據庫等比較慢的IO中獲取,這種情況我們就可以使用原型模式,從其他已有的對象中進行拷貝,而不是每次都創建新對象,進行一些耗時的操作。
原型模式原理
原型模式包含如下幾種角色。
- 抽象原型類(Prototype):它是聲明克隆方法的接口,是所有具體原型類的公共父類,它可以是抽象類也可以是接口;
- 具體原型類(ConcretePrototype):實現在抽象原型類中聲明的克隆方法,在克隆方法中返回自己的一個克隆對象;
- 客戶類(Client):在客戶類中,讓一個原型對象克隆自身從而創建一個新的對象。由于客戶類針對抽象原型類Prototype編程,因此用戶可以根據需要選擇具體原型類。系統具有較好的擴展性,增加或者替換具體原型類都比較方便。
深克隆與淺克隆
根據在復制原型對象的同時是否復制包含在原型對象中引用類型的成員變量 這個條件,原型模式的克隆機制分為兩種,即淺克隆(Shallow Clone)和深克隆(Deep Clone)。
1) 什么是淺克隆
被復制對象的所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象(克隆對象與原型對象共享引用數據類型變量)。
2) 什么是深克隆
除去那些引用其他對象的變量,被復制對象的所有變量都含有與原來的對象相同的值。那些引用其他對象的變量將指向被復制過的新對象,而不再是原有的那些被引用的對象。換言之,深復制把要復制的對象所引用的對象都復制了一遍。
Java中的Object類中提供了 clone()
方法來實現淺克隆。需要注意的是要想實現克隆的 Java 類必須實現一個標識接口 Cloneable ,來表示這個Java類支持被復制。
Cloneable接口是上面的類圖中的抽象原型類,而實現了Cloneable接口的子實現類就是具體的原型類。代碼如下:
3) 淺克隆代碼實現:
public class ConcretePrototype implements Cloneable {public ConcretePrototype() {System.out.println("具體的原型對象創建完成!");}@Overrideprotected ConcretePrototype clone() throws CloneNotSupportedException {System.out.println("具體的原型對象復制成功!");return (ConcretePrototype)super.clone();}
}
測試
@Testpublic void test01() throws CloneNotSupportedException {ConcretePrototype c1 = new ConcretePrototype();ConcretePrototype c2 = c1.clone();System.out.println("對象c1和c2是同一個對象?" + (c1 == c2));}
4) 深克隆代碼實現
在ConcretePrototype類中添加一個對象屬性為Person類型
public class ConcretePrototype implements Cloneable {private Person person;public Person getPerson() {return person;}public void setPerson(Person person) {this.person = person;}void show(){System.out.println("嫌疑人姓名: " +person.getName());}public ConcretePrototype() {System.out.println("具體的原型對象創建完成!");}@Overrideprotected ConcretePrototype clone() throws CloneNotSupportedException {System.out.println("具體的原型對象復制成功!");return (ConcretePrototype)super.clone();}}public class Person {private String name;public Person() {}public Person(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}
測試
@Testpublic void test02() throws CloneNotSupportedException {ConcretePrototype c1 = new ConcretePrototype();Person p1 = new Person();c1.setPerson(p1);//復制c1ConcretePrototype c2 = c1.clone();//獲取復制對象c2中的Person對象Person p2 = c2.getPerson();p2.setName("峰哥");//判斷p1與p2是否是同一對象System.out.println("p1和p2是同一個對象?" + (p1 == p2));c1.show();c2.show();}
打印結果
說明: p1與p2是同一對象,這是淺克隆的效果,也就是對具體原型類中的引用數據類型的屬性進行引用的復制。
如果有需求場景中不允許共享同一對象,那么就需要使用深拷貝,如果想要進行深拷貝需要使用到對象序列化流 (對象序列化之后,再進行反序列化獲取到的是不同對象)。 代碼如下:
@Testpublic void test03() throws Exception {ConcretePrototype c1 = new ConcretePrototype();Person p1 = new Person("峰哥");c1.setPerson(p1);//創建對象序列化輸出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("c.txt"));//將c1對象寫到文件中oos.writeObject(c1);oos.close();//創建對象序列化輸入流ObjectInputStream ois = new ObjectInputStream(new FileInputStream("c.txt"));//讀取對象ConcretePrototype c2 = (ConcretePrototype) ois.readObject();Person p2 = c2.getPerson();p2.setName("凡哥");//判斷p1與p2是否是同一個對象System.out.println("p1和p2是同一個對象?" + (p1 == p2));c1.show();c2.show();}
打印結果:
注意:ConcretePrototype類和Person類必須實現Serializable接口,否則會拋NotSerializableException異常。
其實現在不推薦大家用Cloneable接口,實現比較麻煩,現在借助Apache Commons或者springframework可以直接實現:
- 淺克隆:
BeanUtils.cloneBean(Object obj);BeanUtils.copyProperties(S,T);
- 深克隆:
SerializationUtils.clone(T object);
BeanUtils是利用反射原理獲得所有類可見的屬性和方法,然后復制到target類。
SerializationUtils.clone()就是使用我們的前面講的序列化實現深克隆,當然你要把要克隆的類實現Serialization接口。
4.5.4 原型模式應用實例
模擬某銀行電子賬單系統的廣告信發送功能,廣告信的發送都是有一個模板的,從數據庫查出客戶的信息,然后放到模板中生成一份完整的郵件,然后交給發送機進行發送處理。
發送廣告信郵件UML類圖
代碼實現
- 廣告模板代碼
/*** 廣告信模板代碼**/
public class AdvTemplate {//廣告信名稱private String advSubject = "xx銀行本月還款達標,可抽iPhone 13等好禮!";//廣告信內容private String advContext = "達標用戶請在2022年3月1日到2022年3月30參與抽獎......";public String getAdvSubject() {return this.advSubject;}public String getAdvContext() {return this.advContext;}
}
- 郵件類代碼
package com.mashibing.example01;/*** 郵件類**/
public class Mail {//收件人private String receiver;//郵件名稱private String subject;//稱謂private String appellation;//郵件內容private String context;//郵件尾部, 一般是"xxx版權所有"等信息private String tail;//構造函數public Mail(AdvTemplate advTemplate) {this.context = advTemplate.getAdvContext();this.subject = advTemplate.getAdvSubject();}public String getReceiver() {return receiver;}public void setReceiver(String receiver) {this.receiver = receiver;}public String getSubject() {return subject;}public void setSubject(String subject) {this.subject = subject;}public String getAppellation() {return appellation;}public void setAppellation(String appellation) {this.appellation = appellation;}public String getContext() {return context;}public void setContext(String context) {this.context = context;}public String getTail() {return tail;}public void setTail(String tail) {this.tail = tail;}
}
- 客戶類
/*** 業務場景類**/
public class Client {//發送信息的是數量,這個值可以從數據庫獲取private static int MAX_COUNT = 6;//發送郵件public static void sendMail(Mail mail){System.out.println("標題: " + mail.getSubject() + "\t收件人: " + mail.getReceiver()+ "\t..發送成功!");}public static void main(String[] args) {//模擬郵件發送int i = 0;//把模板定義出來,數據是從數據庫獲取的Mail mail = new Mail(new AdvTemplate());mail.setTail("xxx銀行版權所有");while(i < MAX_COUNT){//下面是每封郵件不同的地方mail.setAppellation(" 先生 (女士)");Random random = new Random();int num = random.nextInt(9999999);mail.setReceiver(num+"@"+"liuliuqiu.com");//發送 郵件sendMail(mail);i++;}}
}
- 運行結果
上面的代碼存在的問題:
- 發送郵件需要重復創建Mail類對象,而且Mail類的不同對象之間差別非常小,這樣重復的創建操作十分的浪費資源;
- 這種情況我們就可以使用原型模式,從其他已有的對象中進行拷貝,而不是每次都創建新對象,進行一些耗時的操作。
代碼重構
- Mail類
/*** 郵件類 實現Cloneable接口,表示該類的實例可以被復制* @author spikeCong* @date 2022/9/20**/
public class Mail implements Cloneable{//收件人private String receiver;//郵件名稱private String subject;//稱謂private String appellation;//郵件內容private String context;//郵件尾部, 一般是"xxx版權所有"等信息private String tail;//構造函數public Mail(AdvTemplate advTemplate) {this.context = advTemplate.getAdvContext();this.subject = advTemplate.getAdvSubject();}public String getReceiver() {return receiver;}public void setReceiver(String receiver) {this.receiver = receiver;}public String getSubject() {return subject;}public void setSubject(String subject) {this.subject = subject;}public String getAppellation() {return appellation;}public void setAppellation(String appellation) {this.appellation = appellation;}public String getContext() {return context;}public void setContext(String context) {this.context = context;}public String getTail() {return tail;}public void setTail(String tail) {this.tail = tail;}@Overridepublic Mail clone(){Mail mail = null;try {mail = (Mail)super.clone();} catch (CloneNotSupportedException e) {e.printStackTrace();}return mail;}
}
- Client類
/*** 業務場景類* @author spikeCong* @date 2022/9/20**/
public class Client {//發送信息的是數量,這個值可以從數據庫獲取private static int MAX_COUNT = 6;//發送郵件public static void sendMail(Mail mail){System.out.println("標題: " + mail.getSubject() + "\t收件人: " + mail.getReceiver()+ "\t..發送成功!");}public static void main(String[] args) {//模擬郵件發送int i = 0;//把模板定義出來,數據是從數據庫獲取的Mail mail = new Mail(new AdvTemplate());mail.setTail("xxx銀行版權所有");while(i < MAX_COUNT){//下面是每封郵件不同的地方Mail cloneMail = mail.clone();cloneMail.setAppellation(" 先生 (女士)");Random random = new Random();int num = random.nextInt(9999999);cloneMail.setReceiver(num+"@"+"liuliuqiu.com");//發送 郵件sendMail(cloneMail);i++;}}
}
4.5.5 原型模式總結
原型模式的優點
-
當創建新的對象實例較為復雜時,使用原型模式可以簡化對象的創建過程,通過復制一個已有實例可以提高新實例的創建效率。
比如,在 AI 系統中,我們經常需要頻繁使用大量不同分類的數據模型文件,在對這一類文件建立對象模型時,不僅會長時間占用 IO 讀寫資源,還會消耗大量 CPU 運算資源,如果頻繁創建模型對象,就會很容易造成服務器 CPU 被打滿而導致系統宕機。通過原型模式我們可以很容易地解決這個問題,當我們完成對象的第一次初始化后,新創建的對象便使用對象拷貝(在內存中進行二進制流的拷貝),雖然拷貝也會消耗一定資源,但是相比初始化的外部讀寫和運算來說,內存拷貝消耗會小很多,而且速度快很多。
-
原型模式提供了簡化的創建結構,工廠方法模式常常需要有一個與產品類等級結構相同的工廠等級結構(具體工廠對應具體產品),而原型模式就不需要這樣,原型模式的產品復制是通過封裝在原型類中的克隆方法實現的,無須專門的工廠類來創建產品。
-
可以使用深克隆的方式保存對象狀態,使用原型模式將對象復制一份并將其狀態保存起來,以便在需要的時候使用,比如恢復到某一歷史狀態,可以輔助實現撤銷操作。
在某些需要保存歷史狀態的場景中,比如,聊天消息、上線發布流程、需要撤銷操作的程序等,原型模式能快速地復制現有對象的狀態并留存副本,方便快速地回滾到上一次保存或最初的狀態,避免因網絡延遲、誤操作等原因而造成數據的不可恢復。
原型模式缺點
- 需要為每一個類配備一個克隆方法,而且該克隆方法位于一個類的內部,當對已有的類進行改造時需要修改源代碼,違背了開閉原則。
使用場景
原型模式常見的使用場景有以下六種。
-
資源優化場景。也就是當進行對象初始化需要使用很多外部資源時,比如,IO 資源、數據文件、CPU、網絡和內存等。
-
復雜的依賴場景。 比如,F 對象的創建依賴 A,A 又依賴 B,B 又依賴 C……于是創建過程是一連串對象的 get 和 set。
-
性能和安全要求的場景。 比如,同一個用戶在一個會話周期里,可能會反復登錄平臺或使用某些受限的功能,每一次訪問請求都會訪問授權服務器進行授權,但如果每次都通過 new 產生一個對象會非常煩瑣,這時則可以使用原型模式。
-
同一個對象可能被多個修改者使用的場景。 比如,一個商品對象需要提供給物流、會員、訂單等多個服務訪問,而且各個調用者可能都需要修改其值時,就可以考慮使用原型模式。
-
需要保存原始對象狀態的場景。 比如,記錄歷史操作的場景中,就可以通過原型模式快速保存記錄。