- 秒懂設(shè)計(jì)模式
- 劉韜
- 3784字
- 2021-07-09 10:30:52
第3章 原型

原型模式(Prototype),在制造業(yè)中通常是指大批量生產(chǎn)開(kāi)始之前研發(fā)出的概念模型,并基于各種參數(shù)指標(biāo)對(duì)其進(jìn)行檢驗(yàn),如果達(dá)到了質(zhì)量要求,即可參照這個(gè)原型進(jìn)行批量生產(chǎn)。原型模式達(dá)到以原型實(shí)例創(chuàng)建副本實(shí)例的目的即可,并不需要知道其原始類(lèi),也就是說(shuō),原型模式可以用對(duì)象創(chuàng)建對(duì)象,而不是用類(lèi)創(chuàng)建對(duì)象,以此達(dá)到效率的提升。
3.1 原件與副本
在講原型模式之前,我們先得搞清楚什么是類(lèi)的實(shí)例化。相信大家一定見(jiàn)過(guò)活字印章,如圖3-1所示,當(dāng)我們調(diào)整好需要的日期(初始化參數(shù)),再輕輕一蓋(調(diào)用構(gòu)造方法),一個(gè)實(shí)例化后的日期便躍然紙上了,這個(gè)過(guò)程正類(lèi)似于類(lèi)的實(shí)例化。

圖3-1 印章實(shí)例化的過(guò)程
其實(shí)構(gòu)造一個(gè)對(duì)象的過(guò)程是耗時(shí)耗力的。想必大家一定有過(guò)打印和復(fù)印的經(jīng)歷,為了節(jié)省成本,我們通常會(huì)用打印機(jī)把電子文檔打印到A4紙上(原型實(shí)例化過(guò)程),再用復(fù)印機(jī)把這份紙質(zhì)文稿復(fù)制多份(原型拷貝過(guò)程),這樣既實(shí)惠又高效。那么,對(duì)于第一份打印出來(lái)的原文稿,我們可以稱(chēng)之為“原型文件”,而對(duì)于復(fù)印過(guò)程,我們則可以稱(chēng)之為“原型拷貝”,如圖3-2所示。

圖3-2 對(duì)原文件的復(fù)印
3.2 卡頓的游戲
想必大家已經(jīng)明白了類(lèi)的實(shí)例化與克隆之間的區(qū)別,二者都是在造對(duì)象,但方法絕對(duì)是不同的。原型模式的目的是從原型實(shí)例克隆出新的實(shí)例,對(duì)于那些有非常復(fù)雜的初始化過(guò)程的對(duì)象或者是需要耗費(fèi)大量資源的情況,原型模式是更好的選擇。理論還需與實(shí)踐結(jié)合,下面開(kāi)始實(shí)戰(zhàn)部分,假設(shè)我們準(zhǔn)備設(shè)計(jì)一個(gè)空戰(zhàn)游戲的程序,如圖3-3所示。

圖3-3 空戰(zhàn)游戲
我們這里為了保持簡(jiǎn)單,設(shè)定游戲?yàn)閱未颍簿褪钦f(shuō)主角飛機(jī)只有一架,而敵機(jī)則有很多架,而且可以在屏幕上垂直向下移動(dòng)來(lái)撞擊主角飛機(jī)。具體是如何實(shí)現(xiàn)的呢?其實(shí)非常簡(jiǎn)單,就是程序不停改變其坐標(biāo)并在畫(huà)面上重繪而已。由淺入深,我們先試著寫(xiě)一個(gè)敵機(jī)類(lèi),請(qǐng)參看代碼清單3-1。
小提示
空戰(zhàn)游戲中的主角如果是單個(gè)實(shí)例的話,其實(shí)就用到單例模式了。讀者可以復(fù)習(xí)一下第2章的內(nèi)容,并親自實(shí)戰(zhàn)練習(xí)一下。本章只關(guān)注可以有多個(gè)實(shí)例的敵機(jī)。
代碼清單3-1 敵機(jī)類(lèi)EnemyPlane
1. public class EnemyPlane {
2.
3. private int x;//敵機(jī)橫坐標(biāo)
4. private int y = 0;//敵機(jī)縱坐標(biāo)
5.
6. public EnemyPlane(int x) {//構(gòu)造器
7. this.x = x;
8. }
9.
10. public int getX() {
11. return x;
12. }
13.
14. public int getY() {
15. return y;
16. }
17.
18. public void fly(){//讓敵機(jī)飛
19. y++;//每調(diào)用一次,敵機(jī)飛行時(shí)縱坐標(biāo)+1
20. }
21.
22. }
如代碼清單3-1所示,敵機(jī)類(lèi)EnemyPlane在第6行的敵機(jī)構(gòu)造器方法中對(duì)飛機(jī)的橫坐標(biāo)x進(jìn)行了初始化,而縱坐標(biāo)則固定為0,這是由于敵機(jī)一開(kāi)始是從頂部飛出的。所以其縱坐標(biāo)y必然為0(屏幕左上角坐標(biāo)為[0, 0])。繼續(xù)往下看,敵機(jī)類(lèi)只提供了getter方法而沒(méi)有提供setter方法,也就是說(shuō)我們只能在初始化時(shí)確定好敵機(jī)的橫坐標(biāo)x,之后則不允許再更改坐標(biāo)了。當(dāng)游戲運(yùn)行時(shí),我們只要連續(xù)調(diào)用第18行的飛行方法fly(),便可以讓飛機(jī)像雨點(diǎn)一樣不斷下落。在開(kāi)始繪制敵機(jī)動(dòng)畫(huà)之前,我們首先得實(shí)例化500架敵機(jī),請(qǐng)參看代碼清單3-2。
代碼清單3-2 客戶端類(lèi)Client
1. public class Client {
2.
3. public static void main(String[] args) {
4. List<EnemyPlane> enemyPlanes = new ArrayList<EnemyPlane>();
5.
6. for (int i = 0; i < 500; i++) {
7. //此處于隨機(jī)縱坐標(biāo)處出現(xiàn)敵機(jī)
8. EnemyPlane ep = new EnemyPlane(new Random().nextInt(200));
9. enemyPlanes.add(ep);
10. }
11.
12. }
13.
14. }
如代碼清單3-2所示,我們?cè)诘?行使用了循環(huán)的方式來(lái)批量生產(chǎn)敵機(jī),并使用了“new”關(guān)鍵字來(lái)實(shí)例化敵機(jī),循環(huán)結(jié)束后500架敵機(jī)便統(tǒng)統(tǒng)被加入第4行定義的飛機(jī)列表enemyPlanes中。這種做法看似沒(méi)有任何問(wèn)題,然而效率卻是非常低的。我們知道在游戲畫(huà)面上根本沒(méi)必要同時(shí)出現(xiàn)這么多敵機(jī),而在游戲還未開(kāi)始之前,也就是游戲的加載階段我們就實(shí)例化了這一關(guān)卡的所有500架敵機(jī),這不但使加載速度變慢,而且是對(duì)有限內(nèi)存資源的一種浪費(fèi)。那么到底什么時(shí)候去構(gòu)造敵機(jī)?答案當(dāng)然是懶加載了,也就是按照地圖坐標(biāo),屏幕滾動(dòng)到某一點(diǎn)時(shí)才實(shí)時(shí)構(gòu)造敵機(jī),這樣一來(lái)問(wèn)題就解決了。
然而遺憾的是,懶加載依然會(huì)有性能問(wèn)題,主要原因在于我們使用的“new”關(guān)鍵字進(jìn)行的基于類(lèi)的實(shí)例化過(guò)程,因?yàn)槊考軘硻C(jī)都進(jìn)行全新構(gòu)造的做法是不合適的,其代價(jià)是耗費(fèi)更多的CPU資源,尤其在一些大型游戲中,很多個(gè)線程在不停地運(yùn)轉(zhuǎn)著,CPU資源本身就非常寶貴,此時(shí)若進(jìn)行大量的類(lèi)構(gòu)造與復(fù)雜的初始化工作,必然會(huì)造成游戲卡頓,甚至有可能會(huì)造成系統(tǒng)無(wú)響應(yīng),使游戲體驗(yàn)大打折扣,如圖3-4所示。

圖3-4 系統(tǒng)無(wú)響應(yīng)
3.3 細(xì)胞分裂
硬件永遠(yuǎn)離不開(kāi)優(yōu)秀的軟件,我們絕不允許以糟糕的軟件設(shè)計(jì)對(duì)硬件發(fā)起挑戰(zhàn),因而代碼優(yōu)化勢(shì)在必行。我們思考一下之前的設(shè)計(jì),既然循環(huán)第一次后已經(jīng)實(shí)例化好了一個(gè)敵機(jī)原型,那么之后又何必去重復(fù)這個(gè)構(gòu)造過(guò)程呢?敵機(jī)對(duì)象能否像細(xì)胞分裂一樣自我復(fù)制呢?要解決這些問(wèn)題,原型模式是最好的解決方案了,下面我們對(duì)敵機(jī)類(lèi)進(jìn)行重構(gòu)并讓其支持原型拷貝,請(qǐng)參看代碼清單3-3。
代碼清單3-3 可被克隆的敵機(jī)類(lèi)EnemyPlane
1. public class EnemyPlane implements Cloneable{
2.
3. private int x;//敵機(jī)橫坐標(biāo)
4. private int y = 0;//敵機(jī)縱坐標(biāo)
5.
6. public EnemyPlane(int x) {//構(gòu)造器
7. this.x = x;
8. }
9.
10. public int getX() {
11. return x;
12. }
13.
14. public int getY() {
15. return y;
16. }
17.
18. public void fly(){//讓敵機(jī)飛
19. y++;//每調(diào)用一次,敵機(jī)飛行時(shí)縱坐標(biāo)+1
20. }
21.
22. //此處開(kāi)放setX,是為了讓克隆后的實(shí)例重新修改橫坐標(biāo)
23. public void setX(int x) {
24. this.x = x;
25. }
26.
27. //重寫(xiě)克隆方法
28. @Override
29. public EnemyPlane clone() throws CloneNotSupportedException {
30. return (EnemyPlane)super.clone();
31. }
32.
33. }
如代碼清單3-3所示,我們讓敵機(jī)類(lèi)EnemyPlane實(shí)現(xiàn)了java.lang包中的克隆接口Cloneable,并在第29行的實(shí)現(xiàn)方法中調(diào)用了父類(lèi)Object的克隆方法,如此一來(lái)外部就能夠?qū)Ρ绢?lèi)的實(shí)例進(jìn)行克隆操作了,省去了由類(lèi)而生的再造過(guò)程。還需要注意的是,我們?cè)诘?3行處加入了設(shè)置橫坐標(biāo)方法setX(),使被實(shí)例化后的敵機(jī)對(duì)象依然可以支持坐標(biāo)位置的變更,這是為了保證克隆飛機(jī)的坐標(biāo)位置個(gè)性化。
3.4 克隆工廠
至此,克隆模式其實(shí)已經(jīng)實(shí)現(xiàn)了,我們只需簡(jiǎn)單調(diào)用克隆方法便能更高效地得到一個(gè)全新的實(shí)例副本。為了更方便地生產(chǎn)飛機(jī),我們決定定義一個(gè)敵機(jī)克隆工廠類(lèi),請(qǐng)參看代碼清單3-4。
代碼清單3-4 敵機(jī)克隆工廠類(lèi)EnemyPlaneFactory
1. public class EnemyPlaneFactory {
2.
3. //此處用單例餓漢模式造一個(gè)敵機(jī)原型
4. private static EnemyPlane protoType = new EnemyPlane(200);
5.
6. //獲取敵機(jī)克隆實(shí)例
7. public static EnemyPlane getInstance(int x){
8. EnemyPlane clone = protoType.clone();//復(fù)制原型機(jī)
9. clone.setX(x);//重新設(shè)置克隆機(jī)的x坐標(biāo)
10. return clone;
11. }
12.
13. }
如代碼清單3-4所示,我們?cè)跀硻C(jī)克隆工廠類(lèi)EnemyPlaneFactory中第4行使用了一個(gè)靜態(tài)的敵機(jī)對(duì)象作為原型,并于第7行提供了一個(gè)獲取敵機(jī)實(shí)例的方法getInstance(),其中簡(jiǎn)單地調(diào)用克隆方法得到一個(gè)新的克隆對(duì)象(此處省略了異常捕獲代碼),并將其橫坐標(biāo)重設(shè)為傳入的參數(shù),最后返回此克隆對(duì)象,這樣我們便可輕松獲取一架敵機(jī)的克隆實(shí)例了。
敵機(jī)克隆工廠類(lèi)定義完畢,客戶端代碼就留給讀者自己去實(shí)踐了。但需要注意,一定得使用“懶加載”的方式,如此既可以節(jié)省內(nèi)存空間,又可以確保敵機(jī)的實(shí)例化速度,實(shí)現(xiàn)敵機(jī)的即時(shí)性按需克隆,這樣游戲便再也不會(huì)出現(xiàn)卡頓現(xiàn)象了。
3.5 深拷貝與淺拷貝
最后,在使用原型模式之前,我們還必須得搞清楚淺拷貝和深拷貝這兩個(gè)概念,否則會(huì)對(duì)某些復(fù)雜對(duì)象的克隆結(jié)果感到無(wú)比困惑。讓我們?cè)贁U(kuò)展一下場(chǎng)景,假設(shè)敵機(jī)類(lèi)里有一顆子彈可以發(fā)射并擊殺玩家的飛機(jī),那么敵機(jī)中則包含一顆實(shí)例化好的子彈對(duì)象,請(qǐng)參看代碼清單3-5。
代碼清單3-5 加裝子彈的敵機(jī)類(lèi)EnemyPlane
1. public class EnemyPlane implements Cloneable{
2.
3. private Bullet bullet = new Bullet();
4. private int x;//敵機(jī)橫坐標(biāo)
5. private int y = 0;//敵機(jī)縱坐標(biāo)
6.
7. //之后代碼省略……
8.
9. }
如代碼清單3-5所示,對(duì)于這種復(fù)雜一些的敵機(jī)類(lèi),此時(shí)如果進(jìn)行克隆操作,我們是否能將第3行中的子彈對(duì)象一同成功克隆呢?答案是否定的。我們都知道,Java中的變量分為原始類(lèi)型和引用類(lèi)型,所謂淺拷貝是指只復(fù)制原始類(lèi)型的值,比如橫坐標(biāo)x與縱坐標(biāo)y這種以原始類(lèi)型int定義的值,它們會(huì)被復(fù)制到新克隆出的對(duì)象中。而引用類(lèi)型bullet同樣會(huì)被拷貝,但是請(qǐng)注意這個(gè)操作只是拷貝了地址引用(指針),也就是說(shuō)副本敵機(jī)與原型敵機(jī)中的子彈是同一顆,因?yàn)閮蓚€(gè)同樣的地址實(shí)際指向的內(nèi)存對(duì)象是同一個(gè)bullet對(duì)象。
需要注意的是,克隆方法中調(diào)用父類(lèi)Object的clone方法進(jìn)行的是淺拷貝,所以此處的bullet并沒(méi)有被真正克隆。然而,每架敵機(jī)攜帶的子彈必須要發(fā)射出不同的彈道,這就必然是不同的子彈對(duì)象了,所以此時(shí)原型模式的淺拷貝實(shí)現(xiàn)是無(wú)法滿足需求的,那么該如何改動(dòng)呢?請(qǐng)參看如代碼清單3-6中對(duì)敵機(jī)類(lèi)的深拷貝支持。
代碼清單3-6 支持深拷貝的敵機(jī)類(lèi)EnemyPlane
1. public class EnemyPlane implements Cloneable{
2.
3. private Bullet bullet;
4. private int x;//敵機(jī)橫坐標(biāo)
5. private int y = 0;//敵機(jī)縱坐標(biāo)
6.
7. public EnemyPlane(int x, Bullet bullet) {
8. this.x = x;
9. this.bullet = bullet;
10. }
11.
12. @Override
13. protected EnemyPlane clone() throws CloneNotSupportedException {
14. EnemyPlane clonePlane = (EnemyPlane) super.clone();//克隆出敵機(jī)
15. clonePlane.setBullet(this.bullet.clone());//對(duì)子彈進(jìn)行深拷貝
16. return clonePlane;
17. }
18.
19. //之后代碼省略……
20.
21. }
如代碼清單3-6所示,首先我們?cè)诘?3行的克隆方法clone()中依舊對(duì)敵機(jī)對(duì)象進(jìn)行了克隆操作,緊接著對(duì)敵機(jī)子彈bullet也進(jìn)行了克隆,這就是深拷貝操作。當(dāng)然,此處要注意對(duì)于子彈類(lèi)Bullet同樣也得實(shí)現(xiàn)克隆接口,請(qǐng)讀者自行實(shí)現(xiàn),此處就不再贅述了。
3.6 克隆的本質(zhì)
終于,在我們用克隆模式對(duì)游戲代碼反復(fù)重構(gòu)后,游戲性能得到了極大的提升,流暢的游戲畫(huà)面確保了優(yōu)秀的用戶體驗(yàn)。最后,我們來(lái)看原型模式的類(lèi)結(jié)構(gòu),如圖3-5所示。原型模式的各角色定義如下。
Prototype(原型接口):聲明克隆方法,對(duì)應(yīng)本例程代碼中的Cloneable接口。
ConcretePrototype(原型實(shí)現(xiàn)):原型接口的實(shí)現(xiàn)類(lèi),實(shí)現(xiàn)方法中調(diào)用super.clone()即可得到新克隆的對(duì)象。
Client(客戶端):客戶端只需調(diào)用實(shí)現(xiàn)此接口的原型對(duì)象方法clone(),便可輕松地得到一個(gè)全新的實(shí)例對(duì)象。

圖3-5 原型模式的類(lèi)結(jié)構(gòu)
從類(lèi)到對(duì)象叫作“創(chuàng)建”,而由本體對(duì)象至副本對(duì)象則叫作“克隆”,當(dāng)需要?jiǎng)?chuàng)建多個(gè)類(lèi)似的復(fù)雜對(duì)象時(shí),我們就可以考慮用原型模式。究其本質(zhì),克隆操作時(shí)Java虛擬機(jī)會(huì)進(jìn)行內(nèi)存操作,直接拷貝原型對(duì)象數(shù)據(jù)流生成新的副本對(duì)象,絕不會(huì)拖泥帶水地觸發(fā)一些多余的復(fù)雜操作(如類(lèi)加載、實(shí)例化、初始化等),所以其效率遠(yuǎn)遠(yuǎn)高于“new”關(guān)鍵字所觸發(fā)的實(shí)例化操作。看盡世間煩擾,撥開(kāi)云霧見(jiàn)青天,有時(shí)候“簡(jiǎn)單粗暴”也是一種去繁從簡(jiǎn)、不繞彎路的解決方案。
- 流量的秘密:Google Analytics網(wǎng)站分析與優(yōu)化技巧(第2版)
- Visual FoxPro程序設(shè)計(jì)教程
- PostgreSQL Cookbook
- 算法訓(xùn)練營(yíng):入門(mén)篇(全彩版)
- C和C++安全編碼(原書(shū)第2版)
- Python爬蟲(chóng)開(kāi)發(fā)與項(xiàng)目實(shí)戰(zhàn)
- PLC編程及應(yīng)用實(shí)戰(zhàn)
- WordPress Plugin Development Cookbook(Second Edition)
- 表哥的Access入門(mén):以Excel視角快速學(xué)習(xí)數(shù)據(jù)庫(kù)開(kāi)發(fā)(第2版)
- PHP編程基礎(chǔ)與實(shí)踐教程
- C++語(yǔ)言程序設(shè)計(jì)
- 并行編程方法與優(yōu)化實(shí)踐
- Flink核心技術(shù):源碼剖析與特性開(kāi)發(fā)
- ASP.NET Core and Angular 2
- Spring Web Services 2 Cookbook