【學習難度:★★★☆☆,使用頻率:★★★☆☆】
直接出處:原型模式
梳理和學習:https://github.com/BruceOuyang/boy-design-pattern
簡書日期: 2018/03/07
簡書首頁:http://www.lxweimin.com/p/0fb891a7c5ed
對象的克隆——原型模式(一)
張紀中版《西游記》以出乎意料的造型和雷人的臺詞遭到廣大觀眾朋友的熱議,我們在此對該話題不作過多討論。但無論是哪個版本的《西游記》,孫悟空都是其中的一號雄性主角,關于他(或它)拔毛變小猴的故事幾乎人人皆知,孫悟空可以用猴毛根據自己的形象,復制(又稱“克隆”或“拷貝”)出很多跟自己長得一模一樣的“身外身”來。
在設計模式中也存在一個類似的模式,可以通過一個原型對象克隆出多個一模一樣的對象,該模式稱之為原型模式。
7.1 大同小異的工作周報
Sunny軟件公司一直使用自行開發的一套OA (Office Automatic,辦公自動化)系統進行日常工作辦理,但在使用過程中,越來越多的人對工作周報的創建和編寫模塊產生了抱怨。追其原因,Sunny軟件公司的OA管理員發現,由于某些崗位每周工作存在重復性,工作周報內容都大同小異,如圖7-1工作周報示意圖。這些周報只有一些小地方存在差異,但是現行系統每周默認創建的周報都是空白報表,用戶只能通過重新輸入或不斷復制粘貼來填寫重復的周報內容,極大降低了工作效率,浪費寶貴的時間。如何快速創建相同或者相似的工作周報,成為Sunny公司OA開發人員面臨的一個新問題。
Sunny公司的開發人員通過對問題進行仔細分析,決定按照如下思路對工作周報模塊進行重新設計和實現:
(1)除了允許用戶創建新周報外,還允許用戶將創建好的周報保存為模板;
(2)用戶在再次創建周報時,可以創建全新的周報,還可以選擇合適的模板復制生成一份相同的周報,然后對新生成的周報根據實際情況進行修改,產生新的周報。
只要按照如上兩個步驟進行處理,工作周報的創建效率將得以大大提高。這個過程讓我們想到平時經常進行的兩個電腦基本操作:復制和粘貼,快捷鍵通常為Ctrl + C和Ctrl + V,通過對已有對象的復制和粘貼,我們可以創建大量的相同對象。
如何在一個面向對象系統中實現對象的復制和粘貼呢?不用著急,本章我們介紹的原型模式正為解決此類問題而誕生。
7.2 原型模式概述
在使用原型模式時,我們需要首先創建一個原型對象,再通過復制這個原型對象來創建更多同類型的對象。試想,如果連孫悟空的模樣都不知道,怎么拔毛變小猴子呢?原型模式的定義如下:原型模式(Prototype Pattern):使用原型實例指定創建對象的種類,并且通過拷貝這些原型創建新的對象。原型模式是一種對象創建型模式。
原型模式的工作原理很簡單:將一個原型對象傳給那個要發動創建的對象,這個要發動創建的對象通過請求原型對象拷貝自己來實現創建過程。由于在軟件系統中我們經常會遇到需要創建多個相同或者相似對象的情況,因此原型模式在真實開發中的使用頻率還是非常高的。原型模式是一種“另類”的創建型模式,創建克隆對象的工廠就是原型類自身,工廠方法由克隆方法來實現。
需要注意的是通過克隆方法所創建的對象是全新的對象,它們在內存中擁有新的地址,通常對克隆所產生的對象進行修改對原型對象不會造成任何影響,每一個克隆對象都是相互獨立的。通過不同的方式修改可以得到一系列相似但不完全相同的對象。原型模式的結構如圖7-2所示:
在原型模式結構圖中包含如下幾個角色:
Prototype(抽象原型類):它是聲明克隆方法的接口,是所有具體原型類的公共父類,可以是抽象類也可以是接口,甚至還可以是具體實現類。
ConcretePrototype(具體原型類):它實現在抽象原型類中聲明的克隆方法,在克隆方法中返回自己的一個克隆對象。
Client(客戶類):讓一個原型對象克隆自身從而創建一個新的對象,在客戶類中只需要直接實例化或通過工廠方法等方式創建一個原型對象,再通過調用該對象的克隆方法即可得到多個相同的對象。由于客戶類針對抽象原型類Prototype編程,因此用戶可以根據需要選擇具體原型類,系統具有較好的可擴展性,增加或更換具體原型類都很方便。
原型模式的核心在于如何實現克隆方法,下面將介紹兩種在Java語言中常用的克隆實現方法:
- 通用實現方法
通用的克隆實現方法是在具體原型類的克隆方法中實例化一個與自身類型相同的對象并將其返回,并將相關的參數傳入新創建的對象中,保證它們的成員屬性相同。示意代碼如下所示:
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;
}
}
思考
能否將上述代碼中的clone()方法寫成:public Prototype clone() { return this; }?給出你的理由。
在客戶類中我們只需要創建一個ConcretePrototype對象作為原型對象,然后調用其clone()方法即可得到對應的克隆對象,如下代碼所示:
Prototype obj1 = new ConcretePrototype();
obj1.setAttr("Sunny");
Prototype obj2 = obj1.clone();
這種方法可作為原型模式的通用實現,它與編程語言特性無關,任何面向對象語言都可以使用這種形式來實現對原型的克隆。
- Java語言提供的clone()方法
學過Java語言的人都知道,所有的Java類都繼承自java.lang.Object。事實上,Object類提供一個clone()方法,可以將一個Java對象復制一份。因此在Java中可以直接使用Object提供的clone()方法來實現對象的克隆,Java語言中的原型模式實現很簡單。
需要注意的是能夠實現克隆的Java類必須實現一個標識接口Cloneable,表示這個Java類支持被復制。如果一個類沒有實現這個接口但是調用了clone()方法,Java編譯器將拋出一個CloneNotSupportedException異常。如下代碼所示:
class ConcretePrototype implements Cloneable
{
//……
@Override
public Prototype clone()
{
Object object = null;
try {
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()方法滿足:
對任何對象x,都有x.clone() != x,即克隆對象與原型對象不是同一個對象;
對任何對象x,都有x.clone().getClass() == x.getClass(),即克隆對象與原型對象的類型一樣;
如果對象x的equals()方法定義恰當,那么x.clone().equals(x)應該成立。
為了獲取對象的一份拷貝,我們可以直接利用Object類的clone()方法,具體步驟如下:
在派生類中覆蓋基類的clone()方法,并聲明為public;
在派生類的clone()方法中,調用super.clone();
派生類需實現Cloneable接口。
此時,Object類相當于抽象原型類,所有實現了Cloneable接口的類相當于具體原型類。
對象的克隆——原型模式(二)
7.3 完整解決方案
Sunny公司開發人員決定使用原型模式來實現工作周報的快速創建,快速創建工作周報結構圖如圖7-3所示:
在圖7-3中,WeeklyLog充當具體原型類,Object類充當抽象原型類,clone()方法為原型方法。WeeklyLog類的代碼如下所示:
//工作周報WeeklyLog:具體原型類,考慮到代碼的可讀性和易理解性,只列出部分與模式相關的核心代碼
class WeeklyLog implements Cloneable
{
private String name;
private String date;
private String content;
public void setName(String name) {
this.name = name;
}
public void setDate(String date) {
this.date = date;
}
public void setContent(String content) {
this.content = content;
}
public String getName() {
return (this.name);
}
public String getDate() {
return (this.date);
}
public String getContent() {
return (this.content);
}
//克隆方法clone(),此處使用Java語言提供的克隆機制
public WeeklyLog clone()
{
Object obj = null;
try
{
obj = super.clone();
return (WeeklyLog)obj;
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持復制!");
return null;
}
}
}
編寫如下客戶端測試代碼:
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());
}
}
編譯并運行程序,輸出結果如下:
****周報****
周次:第12周
姓名:張無忌
內容:這周工作很忙,每天加班!
--------------------------------
****周報****
周次:第13周
姓名:張無忌
內容:這周工作很忙,每天加班!
通過已創建的工作周報可以快速創建新的周報,然后再根據需要修改周報,無須再從頭開始創建。原型模式為工作流系統中任務單的快速生成提供了一種解決方案。
思考
如果在Client類的main()函數中增加如下幾條語句:
System.out.println(log_previous == log_new);
System.out.println(log_previous.getDate() == log_new.getDate());
System.out.println(log_previous.getName() == log_new.getName());
System.out.println(log_previous.getContent() == log_new.getContent());
預測這些語句的輸出結果。
對象的克隆——原型模式(三)
7.4 帶附件的周報
通過引入原型模式,Sunny軟件公司OA系統支持工作周報的快速克隆,極大提高了工作周報的編寫效率,受到員工的一致好評。但有員工又發現一個問題,有些工作周報帶有附件,例如經理助理“小龍女”的周報通常附有本周項目進展報告匯總表、本周客戶反饋信息匯總表等,如果使用上述原型模式來復制周報,周報雖然可以復制,但是周報的附件并不能復制,這是由于什么原因導致的呢?如何才能實現周報和附件的同時復制呢?我們在本節將討論如何解決這些問題。
在回答這些問題之前,先介紹一下兩種不同的克隆方法,淺克隆(ShallowClone)和深克隆(DeepClone)。在Java語言中,數據類型分為值類型(基本數據類型)和引用類型,值類型包括int、double、byte、boolean、char等簡單數據類型,引用類型包括類、接口、數組等復雜類型。淺克隆和深克隆的主要區別在于是否支持引用類型的成員變量的復制,下面將對兩者進行詳細介紹。
1.淺克隆
在淺克隆中,如果原型對象的成員變量是值類型,將復制一份給克隆對象;如果原型對象的成員變量是引用類型,則將引用對象的地址復制一份給克隆對象,也就是說原型對象和克隆對象的成員變量指向相同的內存地址。簡單來說,在淺克隆中,當對象被復制時只復制它本身和其中包含的值類型的成員變量,而引用類型的成員對象并沒有復制,如圖7-4所示:
在Java語言中,通過覆蓋Object類的clone()方法可以實現淺克隆。為了讓大家更好地理解淺克隆和深克隆的區別,我們首先使用淺克隆來實現工作周報和附件類的復制,其結構如圖7-5所示:
附件類Attachment代碼如下:
//附件類
class Attachment
{
private String name; //附件名
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
public void download()
{
System.out.println("下載附件,文件名為" + name);
}
}
修改工作周報類WeeklyLog,修改后的代碼如下:
//工作周報WeeklyLog
class WeeklyLog implements Cloneable
{
//為了簡化設計和實現,假設一份工作周報中只有一個附件對象,實際情況中可以包含多個附件,可以通過List等集合對象來實現
private Attachment attachment;
private String name;
private String date;
private String content;
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
public void setName(String name) {
this.name = name;
}
public void setDate(String date) {
this.date = date;
}
public void setContent(String content) {
this.content = content;
}
public Attachment getAttachment(){
return (this.attachment);
}
public String getName() {
return (this.name);
}
public String getDate() {
return (this.date);
}
public String getContent() {
return (this.content);
}
//使用clone()方法實現淺克隆
public WeeklyLog clone()
{
Object obj = null;
try
{
obj = super.clone();
return (WeeklyLog)obj;
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持復制!");
return null;
}
}
}
客戶端代碼如下所示:
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() == log_new.getAttachment()));
}
}
編譯并運行程序,輸出結果如下:
周報是否相同? false
附件是否相同? true
由于使用的是淺克隆技術,因此工作周報對象復制成功,通過“==”比較原型對象和克隆對象的內存地址時輸出false;但是比較附件對象的內存地址時輸出true,說明它們在內存中是同一個對象。
2.深克隆
在深克隆中,無論原型對象的成員變量是值類型還是引用類型,都將復制一份給克隆對象,深克隆將原型對象的所有引用對象也復制一份給克隆對象。簡單來說,在深克隆中,除了對象本身被復制外,對象所包含的所有成員變量也將復制,如圖7-6所示:
在Java語言中,如果需要實現深克隆,可以通過序列化(Serialization)等方式來實現。序列化就是將對象寫到流的過程,寫到流中的對象是原有對象的一個拷貝,而原對象仍然存在于內存中。通過序列化實現的拷貝不僅可以復制對象本身,而且可以復制其引用的成員對象,因此通過序列化將對象寫到一個流中,再從流里將其讀出來,可以實現深克隆。需要注意的是能夠實現序列化的對象其類必須實現Serializable接口,否則無法實現序列化操作。下面我們使用深克隆技術來實現工作周報和附件對象的復制,由于要將附件對象和工作周報對象都寫入流中,因此兩個類均需要實現Serializable接口,其結構如圖7-7所示:
修改后的附件類Attachment代碼如下:
import java.io.*;
//附件類
class Attachment implements Serializable
{
private String name; //附件名
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
public void download()
{
System.out.println("下載附件,文件名為" + name);
}
}
工作周報類WeeklyLog不再使用Java自帶的克隆機制,而是通過序列化來從頭實現對象的深克隆,我們需要重新編寫clone()方法,修改后的代碼如下:
import java.io.*;
//工作周報類
class WeeklyLog implements Serializable
{
private Attachment attachment;
private String name;
private String date;
private String content;
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
public void setName(String name) {
this.name = name;
}
public void setDate(String date) {
this.date = date;
}
public void setContent(String content) {
this.content = content;
}
public Attachment getAttachment(){
return (this.attachment);
}
public String getName() {
return (this.name);
}
public String getDate() {
return (this.date);
}
public String getContent() {
return (this.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();
}
}
客戶端代碼如下所示:
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這些接口的實現類是否具有某個功能,如是否支持克隆、是否支持序列化等。
對象的克隆——原型模式(四)
7.5 原型管理器的引入和實現
原型管理器(Prototype Manager)是將多個原型對象存儲在一個集合中供客戶端使用,它是一個專門負責克隆對象的工廠,其中定義了一個集合用于存儲原型對象,如果需要某個原型對象的一個克隆,可以通過復制集合中對應的原型對象來獲得。在原型管理器中針對抽象原型類進行編程,以便擴展。其結構如圖7-8所示:
下面通過模擬一個簡單的公文管理器來介紹原型管理器的設計與實現: Sunny軟件公司在日常辦公中有許多公文需要創建、遞交和審批,例如《可行性分析報告》、《立項建議書》、《軟件需求規格說明書》、《項目進展報告》等,為了提高工作效率,在OA系統中為各類公文均創建了模板,用戶可以通過這些模板快速創建新的公文,這些公文模板需要統一進行管理,系統根據用戶請求的不同生成不同的新公文。
我們使用帶原型管理器的原型模式實現公文管理器的設計,其結構如圖7-9所示:
以下是實現該功能的一些核心代碼,考慮到代碼的可讀性,我們對所有的類都進行了簡化:
import java.util.*;
//抽象公文接口,也可定義為抽象類,提供clone()方法的實現,將業務方法聲明為抽象方法
interface OfficialDocument extends Cloneable
{
public OfficialDocument clone();
public void display();
}
//可行性分析報告(Feasibility Analysis Report)類
class FAR implements OfficialDocument
{
public OfficialDocument clone()
{
OfficialDocument far = null;
try
{
far = (OfficialDocument)super.clone();
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持復制!");
}
return far;
}
public void display()
{
System.out.println("《可行性分析報告》");
}
}
//軟件需求規格說明書(Software Requirements Specification)類
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("《軟件需求規格說明書》");
}
}
//原型管理器(使用餓漢式單例實現)
class PrototypeManager
{
//定義一個Hashtable,用于存儲原型對象
private Hashtable ht=new Hashtable();
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();
}
public static PrototypeManager getPrototypeManager()
{
return pm;
}
}
客戶端代碼如下所示:
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對象,有利于節省系統資源,并可以更好地對原型管理器對象進行控制。
思考
如果需要增加一種新類型的公文,如《項目進展報告》(Project Progress Report, PPR),公文管理器系統源代碼如何修改,動手實踐你的修改方案。
7.6 原型模式總結
原型模式作為一種快速創建大量相同或相似對象的方式,在軟件開發中應用較為廣泛,很多軟件提供的復制(Ctrl + C)和粘貼(Ctrl + V)操作就是原型模式的典型應用,下面對該模式的使用效果和適用情況進行簡單的總結。
- 主要優點
原型模式的主要優點如下:
(1) 當創建新的對象實例較為復雜時,使用原型模式可以簡化對象的創建過程,通過復制一個已有實例可以提高新實例的創建效率。
(2) 擴展性較好,由于在原型模式中提供了抽象原型類,在客戶端可以針對抽象原型類進行編程,而將具體原型類寫在配置文件中,增加或減少產品類對原有系統都沒有任何影響。
(3) 原型模式提供了簡化的創建結構,工廠方法模式常常需要有一個與產品類等級結構相同的工廠等級結構,而原型模式就不需要這樣,原型模式中產品的復制是通過封裝在原型類中的克隆方法實現的,無須專門的工廠類來創建產品。
(4) 可以使用深克隆的方式保存對象的狀態,使用原型模式將對象復制一份并將其狀態保存起來,以便在需要的時候使用(如恢復到某一歷史狀態),可輔助實現撤銷操作。
- 主要缺點
原型模式的主要缺點如下:
(1) 需要為每一個類配備一個克隆方法,而且該克隆方法位于一個類的內部,當對已有的類進行改造時,需要修改源代碼,違背了“開閉原則”。
(2) 在實現深克隆時需要編寫較為復雜的代碼,而且當對象之間存在多重的嵌套引用時,為了實現深克隆,每一層對象對應的類都必須支持深克隆,實現起來可能會比較麻煩。
- 適用場景
在以下情況下可以考慮使用原型模式:
(1) 創建新對象成本較大(如初始化需要占用較長的時間,占用太多的CPU資源或網絡資源),新的對象可以通過原型模式對已有對象進行復制來獲得,如果是相似對象,則可以對其成員變量稍作修改。
(2) 如果系統要保存對象的狀態,而對象的狀態變化很小,或者對象本身占用內存較少時,可以使用原型模式配合備忘錄模式來實現。
(3) 需要避免使用分層次的工廠類來創建分層次的對象,并且類的實例對象只有一個或很少的幾個組合狀態,通過復制原型對象得到新實例可能比使用構造函數創建一個新實例更加方便。
練習
設計并實現一個客戶類Customer,其中包含一個名為客戶地址的成員變量,客戶地址的類型為Address,用淺克隆和深克隆分別實現Customer對象的復制并比較這兩種克隆方式的異同。
練習會在我的github上做掉