「每天一個知識點」精講設計模式之原型模式

點擊上方"java全棧技術"關注,每天學習一個java知識點

張紀中版《西遊記》以出乎意料的造型和雷人的臺詞遭到廣大觀眾朋友的熱議,我們在此對該話題不作過多討論。但無論是哪個版本的《西遊記》,孫悟空都是其中的一號雄性主角,關於他(或它)拔毛變小猴的故事幾乎人人皆知,孫悟空可以用猴毛根據自己的形象,複製(又稱“克隆”或“拷貝”)出很多跟自己長得一模一樣的“身外身”來。在設計模式中也存在一個類似的模式,可以通過一個原型對象克隆出多個一模一樣的對象,該模式稱之為原型模式。


1 大同小異的工作週報

Sunny軟件公司一直使用自行開發的一套OA (Office Automatic,辦公自動化)系統進行日常工作辦理,但在使用過程中,越來越多的人對工作週報的創建和編寫模塊產生了抱怨。追其原因,Sunny軟件公司的OA管理員發現,由於某些崗位每週工作存在重複性,工作週報內容都大同小異,如圖7-1工作週報示意圖。這些週報只有一些小地方存在差異,但是現行系統每週默認創建的週報都是空白報表,用戶只能通過重新輸入或不斷複製粘貼來填寫重複的週報內容,極大降低了工作效率,浪費寶貴的時間。如何快速創建相同或者相似的工作週報,成為Sunny公司OA開發人員面臨的一個新問題。

「每天一個知識點」精講設計模式之原型模式

圖1 工作週報示意圖

Sunny公司的開發人員通過對問題進行仔細分析,決定按照如下思路對工作週報模塊進行重新設計和實現:

(1)除了允許用戶創建新週報外,還允許用戶將創建好的週報保存為模板;

(2)用戶在再次創建週報時,可以創建全新的週報,還可以選擇合適的模板複製生成一份相同的週報,然後對新生成的週報根據實際情況進行修改,產生新的週報。

只要按照如上兩個步驟進行處理,工作週報的創建效率將得以大大提高。這個過程讓我們想到平時經常進行的兩個電腦基本操作:複製和粘貼,快捷鍵通常為Ctrl + C和Ctrl + V,通過對已有對象的複製和粘貼,我們可以創建大量的相同對象。如何在一個面向對象系統中實現對象的複製和粘貼呢?不用著急,本章我們介紹的原型模式正為解決此類問題而誕生。

2 原型模式概述

在使用原型模式時,我們需要首先創建一個原型對象,再通過複製這個原型對象來創建更多同類型的對象。試想,如果連孫悟空的模樣都不知道,怎麼拔毛變小猴子呢?原型模式的定義如下:

原型模式(Prototype Pattern):使用原型實例指定創建對象的種類,並且通過拷貝這些原型創建新的對象。原型模式是一種對象創建型模式。

原型模式的工作原理很簡單:將一個原型對象傳給那個要發動創建的對象,這個要發動創建的對象通過請求原型對象拷貝自己來實現創建過程。由於在軟件系統中我們經常會遇到需要創建多個相同或者相似對象的情況,因此原型模式在真實開發中的使用頻率還是非常高的。原型模式是一種“另類”的創建型模式,創建克隆對象的工廠就是原型類自身,工廠方法由克隆方法來實現。

需要注意的是通過克隆方法所創建的對象是全新的對象,它們在內存中擁有新的地址,通常對克隆所產生的對象進行修改對原型對象不會造成任何影響,每一個克隆對象都是相互獨立的。通過不同的方式修改可以得到一系列相似但不完全相同的對象。

原型模式的結構如圖7-2所示:

「每天一個知識點」精講設計模式之原型模式

圖2 原型模式結構圖

在原型模式結構圖中包含如下幾個角色:

●Prototype(抽象原型類):它是聲明克隆方法的接口,是所有具體原型類的公共父類,可以是抽象類也可以是接口,甚至還可以是具體實現類。

● ConcretePrototype(具體原型類):它實現在抽象原型類中聲明的克隆方法,在克隆方法中返回自己的一個克隆對象。

● Client(客戶類):讓一個原型對象克隆自身從而創建一個新的對象,在客戶類中只需要直接實例化或通過工廠方法等方式創建一個原型對象,再通過調用該對象的克隆方法即可得到多個相同的對象。由於客戶類針對抽象原型類Prototype編程,因此用戶可以根據需要選擇具體原型類,系統具有較好的可擴展性,增加或更換具體原型類都很方便。

原型模式的核心在於如何實現克隆方法,下面將介紹兩種在Java語言中常用的克隆實現方法:

1.通用實現方法

通用的克隆實現方法是在具體原型類的克隆方法中實例化一個與自身類型相同的對象並將其返回,並將相關的參數傳入新創建的對象中,保證它們的成員屬性相同。示意代碼如下所示:

「每天一個知識點」精講設計模式之原型模式

在客戶類中我們只需要創建一個ConcretePrototype對象作為原型對象,然後調用其clone()方法即可得到對應的克隆對象,如下代碼所示:

「每天一個知識點」精講設計模式之原型模式

這種方法可作為原型模式的通用實現,它與編程語言特性無關,任何面嚮對象語言都可以使用這種形式來實現對原型的克隆。

2. Java語言提供的clone()方法

學過Java語言的人都知道,所有的Java類都繼承自java.lang.Object。事實上,Object類提供一個clone()方法,可以將一個Java對象複製一份。因此在Java中可以直接使用Object提供的clone()方法來實現對象的克隆,Java語言中的原型模式實現很簡單。

需要注意的是能夠實現克隆的Java類必須實現一個標識接口Cloneable,表示這個Java類支持被複制。如果一個類沒有實現這個接口但是調用了clone()方法,Java編譯器將拋出一個CloneNotSupportedException異常。如下代碼所示:

「每天一個知識點」精講設計模式之原型模式

在客戶端創建原型對象和克隆對象也很簡單,如下代碼所示:

「每天一個知識點」精講設計模式之原型模式

一般而言,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接口的類相當於具體原型類。

3 完整解決方案

Sunny公司開發人員決定使用原型模式來實現工作週報的快速創建,快速創建工作週報結構圖如圖7-3所示:

「每天一個知識點」精講設計模式之原型模式

圖3 快速創建工作週報結構圖

在圖3中,WeeklyLog充當具體原型類,Object類充當抽象原型類,clone()方法為原型方法。WeeklyLog類的代碼如下所示:

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

編寫如下客戶端測試代碼:

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

編譯並運行程序,輸出結果如下:

「每天一個知識點」精講設計模式之原型模式

通過已創建的工作週報可以快速創建新的週報,然後再根據需要修改週報,無須再從頭開始創建。原型模式為工作流系統中任務單的快速生成提供了一種解決方案。

思考

如果在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());

預測這些語句的輸出結果。

4 帶附件的週報

通過引入原型模式,Sunny軟件公司OA系統支持工作週報的快速克隆,極大提高了工作週報的編寫效率,受到員工的一致好評。但有員工又發現一個問題,有些工作週報帶有附件,例如經理助理“小龍女”的週報通常附有本週項目進展報告彙總表、本週客戶反饋信息彙總表等,如果使用上述原型模式來複制週報,週報雖然可以複製,但是週報的附件並不能複製,這是由於什麼原因導致的呢?如何才能實現週報和附件的同時複製呢?我們在本節將討論如何解決這些問題。

在回答這些問題之前,先介紹一下兩種不同的克隆方法,淺克隆(ShallowClone)和深克隆(DeepClone)。在Java語言中,數據類型分為值類型(基本數據類型)和引用類型,值類型包括int、double、byte、boolean、char等簡單數據類型,引用類型包括類、接口、數組等複雜類型。淺克隆和深克隆的主要區別在於是否支持引用類型的成員變量的複製,下面將對兩者進行詳細介紹。

1.淺克隆

在淺克隆中,如果原型對象的成員變量是值類型,將複製一份給克隆對象;如果原型對象的成員變量是引用類型,則將引用對象的地址複製一份給克隆對象,也就是說原型對象和克隆對象的成員變量指向相同的內存地址。簡單來說,在淺克隆中,當對象被複制時只複製它本身和其中包含的值類型的成員變量,而引用類型的成員對象並沒有複製,如圖4所示:

「每天一個知識點」精講設計模式之原型模式

圖4 淺克隆示意圖

在Java語言中,通過覆蓋Object類的clone()方法可以實現淺克隆。為了讓大家更好地理解淺克隆和深克隆的區別,我們首先使用淺克隆來實現工作週報和附件類的複製,其結構如圖7-5所示:

「每天一個知識點」精講設計模式之原型模式

圖5 帶附件的週報結構圖(淺克隆)

附件類Attachment代碼如下:

「每天一個知識點」精講設計模式之原型模式

修改工作週報類WeeklyLog,修改後的代碼如下:

「每天一個知識點」精講設計模式之原型模式

客戶端代碼如下所示:

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

編譯並運行程序,輸出結果如下:

週報是否相同? false

附件是否相同? true

由於使用的是淺克隆技術,因此工作週報對象複製成功,通過“==”比較原型對象和克隆對象的內存地址時輸出false;但是比較附件對象的內存地址時輸出true,說明它們在內存中是同一個對象。

2.深克隆

在深克隆中,無論原型對象的成員變量是值類型還是引用類型,都將複製一份給克隆對象,深克隆將原型對象的所有引用對象也複製一份給克隆對象。簡單來說,在深克隆中,除了對象本身被複制外,對象所包含的所有成員變量也將複製,如圖6所示:

「每天一個知識點」精講設計模式之原型模式

圖6 深克隆示意圖

在Java語言中,如果需要實現深克隆,可以通過序列化(Serialization)等方式來實現。序列化就是將對象寫到流的過程,寫到流中的對象是原有對象的一個拷貝,而原對象仍然存在於內存中。通過序列化實現的拷貝不僅可以複製對象本身,而且可以複製其引用的成員對象,因此通過序列化將對象寫到一個流中,再從流裡將其讀出來,可以實現深克隆。需要注意的是能夠實現序列化的對象其類必須實現Serializable接口,否則無法實現序列化操作。下面我們使用深克隆技術來實現工作週報和附件對象的複製,由於要將附件對象和工作週報對象都寫入流中,因此兩個類均需要實現Serializable接口,其結構如圖7所示:

「每天一個知識點」精講設計模式之原型模式

圖7 帶附件的週報結構圖(深克隆)

修改後的附件類Attachment代碼如下:

「每天一個知識點」精講設計模式之原型模式

工作週報類WeeklyLog不再使用Java自帶的克隆機制,而是通過序列化來從頭實現對象的深克隆,我們需要重新編寫clone()方法,修改後的代碼如下:

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

客戶端代碼如下所示:

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

編譯並運行程序,輸出結果如下:

週報是否相同? false

附件是否相同? false

從輸出結果可以看出,由於使用了深克隆技術,附件對象也得以複製,因此用“==”比較原型對象的附件和克隆對象的附件時輸出結果均為false。深克隆技術實現了原型對象和克隆對象的完全獨立,對任意克隆對象的修改都不會給其他對象產生影響,是一種更為理想的克隆實現方式。

擴展

Java語言提供的Cloneable接口和Serializable接口的代碼非常簡單,它們都是空接口,這種空接口也稱為標識接口,標識接口中沒有任何方法的定義,其作用是告訴JRE這些接口的實現類是否具有某個功能,如是否支持克隆、是否支持序列化等。

5 原型管理器的引入和實現

原型管理器(Prototype Manager)是將多個原型對象存儲在一個集合中供客戶端使用,它是一個專門負責克隆對象的工廠,其中定義了一個集合用於存儲原型對象,如果需要某個原型對象的一個克隆,可以通過複製集合中對應的原型對象來獲得。在原型管理器中針對抽象原型類進行編程,以便擴展。其結構如圖8所示:

「每天一個知識點」精講設計模式之原型模式

圖8 帶原型管理器的原型模式

下面通過模擬一個簡單的公文管理器來介紹原型管理器的設計與實現:

Sunny軟件公司在日常辦公中有許多公文需要創建、遞交和審批,例如《可行性分析報告》、《立項建議書》、《軟件需求規格說明書》、《項目進展報告》等,為了提高工作效率,在OA系統中為各類公文均創建了模板,用戶可以通過這些模板快速創建新的公文,這些公文模板需要統一進行管理,系統根據用戶請求的不同生成不同的新公文。

我們使用帶原型管理器的原型模式實現公文管理器的設計,其結構如圖9所示:

「每天一個知識點」精講設計模式之原型模式

圖9 公文管理器結構圖

以下是實現該功能的一些核心代碼,考慮到代碼的可讀性,我們對所有的類都進行了簡化:

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

客戶端代碼如下所示:

「每天一個知識點」精講設計模式之原型模式

「每天一個知識點」精講設計模式之原型模式

編譯並運行程序,輸出結果如下:

《可行性分析報告》

《可行性分析報告》

false

《軟件需求規格說明書》

《軟件需求規格說明書》

false

在PrototypeManager中定義了一個Hashtable類型的集合對象,使用“鍵值對”來存儲原型對象,客戶端可以通過Key(如“far”或“srs”)來獲取對應原型對象的克隆對象。PrototypeManager類提供了類似工廠方法的getOfficialDocument()方法用於返回一個克隆對象。在本實例代碼中,我們將PrototypeManager設計為單例類,使用餓漢式單例實現,確保系統中有且僅有一個PrototypeManager對象,有利於節省系統資源,並可以更好地對原型管理器對象進行控制。

思考

如果需要增加一種新類型的公文,如《項目進展報告》(Project Progress Report, PPR),公文管理器系統源代碼如何修改,動手實踐你的修改方案。

6 原型模式總結

原型模式作為一種快速創建大量相同或相似對象的方式,在軟件開發中應用較為廣泛,很多軟件提供的複製(Ctrl + C)和粘貼(Ctrl + V)操作就是原型模式的典型應用,下面對該模式的使用效果和適用情況進行簡單的總結。

1.主要優點

原型模式的主要優點如下:

(1) 當創建新的對象實例較為複雜時,使用原型模式可以簡化對象的創建過程,通過複製一個已有實例可以提高新實例的創建效率。

(2) 擴展性較好,由於在原型模式中提供了抽象原型類,在客戶端可以針對抽象原型類進行編程,而將具體原型類寫在配置文件中,增加或減少產品類對原有系統都沒有任何影響。

(3) 原型模式提供了簡化的創建結構,工廠方法模式常常需要有一個與產品類等級結構相同的工廠等級結構,而原型模式就不需要這樣,原型模式中產品的複製是通過封裝在原型類中的克隆方法實現的,無須專門的工廠類來創建產品。

(4) 可以使用深克隆的方式保存對象的狀態,使用原型模式將對象複製一份並將其狀態保存起來,以便在需要的時候使用(如恢復到某一歷史狀態),可輔助實現撤銷操作。

2.主要缺點

原型模式的主要缺點如下:

(1) 需要為每一個類配備一個克隆方法,而且該克隆方法位於一個類的內部,當對已有的類進行改造時,需要修改源代碼,違背了“開閉原則”。

(2) 在實現深克隆時需要編寫較為複雜的代碼,而且當對象之間存在多重的嵌套引用時,為了實現深克隆,每一層對象對應的類都必須支持深克隆,實現起來可能會比較麻煩。

3.適用場景

在以下情況下可以考慮使用原型模式:

(1) 創建新對象成本較大(如初始化需要佔用較長的時間,佔用太多的CPU資源或網絡資源),新的對象可以通過原型模式對已有對象進行復制來獲得,如果是相似對象,則可以對其成員變量稍作修改。

(2) 如果系統要保存對象的狀態,而對象的狀態變化很小,或者對象本身佔用內存較少時,可以使用原型模式配合備忘錄模式來實現。

(3) 需要避免使用分層次的工廠類來創建分層次的對象,並且類的實例對象只有一個或很少的幾個組合狀態,通過複製原型對象得到新實例可能比使用構造函數創建一個新實例更加方便。

練習

設計並實現一個客戶類Customer,其中包含一個名為客戶地址的成員變量,客戶地址的類型為Address,用淺克隆和深克隆分別實現Customer對象的複製並比較這兩種克隆方式的異同。

原文:http://blog.csdn.net/lovelion


分享到:


相關文章: