官术网_书友最值得收藏!

原型模式(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á)到效率的提升。

在講原型模式之前,我們先得搞清楚什么是類(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ù)印

想必大家已經(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)

硬件永遠(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è)性化。

至此,克隆模式其實(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)象了。

最后,在使用原型模式之前,我們還必須得搞清楚淺拷貝和深拷貝這兩個(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),此處就不再贅述了。

終于,在我們用克隆模式對(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)、不繞彎路的解決方案。

主站蜘蛛池模板: 临沧市| 宝清县| 濮阳市| 罗田县| 黔南| 菏泽市| 新泰市| 永兴县| 自治县| 姚安县| 绥中县| 盖州市| 贵溪市| 长春市| 云龙县| 江安县| 永州市| 黄浦区| 迁安市| 常州市| 会宁县| 京山县| 偃师市| 泰州市| 通城县| 蓝山县| 阿鲁科尔沁旗| 绥德县| 永吉县| 石屏县| 东至县| 庆阳市| 盘锦市| 鄱阳县| 墨脱县| 宜黄县| 昭通市| 西丰县| 方城县| 丰宁| 芜湖县|