原型模式(Prototype),在制造業種通常是指大批量生產開始之前研發出的概念模型,并基于各種參數指標對其進行檢驗,效果達到了質量要求,即可參照這個原型進行批量生產。即,原型模式可以用對象創建對象,而不是用類創建對象,以此達到效率的提升。
舉個栗子,類似于打印機和復印機的區別:
- 第一份打印出來的原文稿,我們稱之為“原型文件”
- 對于復印過程,我們稱之為“原型拷貝”
原型模式對于 非常復雜初始化過程的對象,或者是?需要消耗大量資源?的情況下,原型模式是更好的選擇。
目錄
一、以空戰游戲為例
1. 敵機類?EnemyPlane 代碼:
2. 怎樣創建500臺敵機
1)??for循環批量生產敵機
2)??懶加載依然有性能問題
3)??細胞分裂
3. 復雜對象的克隆?- 深拷貝、淺拷貝
4. 克隆的本質
一、以空戰游戲為例
假設我們設計一個空戰游戲的程序,
為了簡單,我們設定游戲為單打,也就是說主角飛機只有一駕,而敵機有很多駕,而且可以在屏幕上垂直向下移動來撞擊注解飛機,具體是怎樣實現的呢?其實比較簡單,就是程序不停改變坐標并且在畫面上重繪而已。由淺入深,我們?先試著寫一個敵機類。
Tips:空戰游戲中的主角如果是單個實例的話,其實就用到單例模式了。可以參考:?設計模式 - 單例模式-CSDN博客,本文只關注可以有多個實例的敵機。
1. 敵機類?EnemyPlane 代碼:
public class EnemyPlane {private int x; // 敵機橫坐標private int y = 0; // 敵機縱坐標public EnemyPlane(int x) { // 構造器this.x = x;}public int getX() {return x;}public int getY() {return y;}public void fly() { // 讓敵機飛y++; // 每調用一次,敵機飛行時縱坐標+1}
}
縱坐標固定為0,由于敵機一開始是從頂部飛出去的。
只有getter沒有setter,也就是只能在初始化時確定好敵機的橫坐標x,之后不允許改了
2. 怎樣創建500臺敵機
我們想讓敵機向雨點一樣不斷下落,首先需要實例化500駕敵機。
1)??for循環批量生產敵機
這樣做法看似沒有問題,實際上效率非常低。
游戲畫面不可能同時出現500駕敵機,而且在游戲未開始的時候就加載了500駕,不僅使加載速度變慢、也是對有限內存資源的一種浪費。
public class Client {public static void main(String[] args) {List<EnemyPlane> enemyPlanes = new ArrayList<EnemyPlane>();for (int i = 0; i < 500; i++) {// 此處于隨機縱坐標處出現敵機EnemyPlane ep = new EnemyPlane(new Random().nextInt(200));enemyPlanes.add(ep);}}
}
那么,到底什么時候才去構造敵機,-- 當然是懶加載了
按照地圖坐標,屏幕滾動到某一點時才實時構造敵機,就解決問題了。
2)??懶加載依然有性能問題
主要原因在于,“new” 關鍵字進行的基于類的實例化過程,每駕敵機都進行全新構造的做法是不合適的,其代價是耗費更多的CPU資源。
尤其大型游戲中,很多個線程不停運轉著,CPU資源本身就非常寶貴,此時如果進行大量的類構造與復雜的初始化工作,必然會造成游戲卡頓、甚至會造成系統無響應
3)??細胞分裂
硬件永遠離不開優秀的軟件,我們絕不允許以糟糕的軟件設計對硬件發起挑戰。
既然循環第一次之后已經實例化好了一個敵機原型,那么之后又何必去重復這個構造過程呢?敵機對象是否能像細胞分裂一樣自我復制呢?要解決這個問題,原型模式是最好的解決方案了。
1)重構敵機類,支持原型拷貝
讓敵機類EnemyPlane實現了java.lang包中的克隆接口Cloneable,并在實現方法中調用了父類Object的克隆方法,省去了由類而生的再造過程。
public class EnemyPlane implements Cloneable {private int x; // 敵機橫坐標private int y = 0; // 敵機縱坐標public EnemyPlane(int x) { // 構造器this.x = x;}public int getX() {return x;}public int getY() {return y;}public void fly() { // 讓敵機飛y++; // 每調用一次,敵機飛行時縱坐標+1}// 此處開放setX,是為了讓克隆后的實例重新修改橫坐標public void setX(int x) {this.x = x;}// 重寫克隆方法@Overridepublic EnemyPlane clone() throws CloneNotSupportedException {return (EnemyPlane)super.clone();}
}
至此,克隆模式其實已經實現了,只需簡單的調用克隆方法即可更高效地得到一個全新的實例副本。
為了更方便的生產飛機,我們決定定義一個敵機克隆工廠類
public class EnemyPlaneFactory {// 此處用單例模式創建一個敵機原型private static EnemyPlane protoType = new EnemyPlane(200);// 獲取敵機克隆實例public static EnemyPlane getInstance(int x) {EnemyPlane clone = protoType.clone(); // 復制原型機clone.setX(x); // 重新設置克隆機的x坐標return clone;}
}
我們在敵機克隆工廠類EnemyPlaneFactory中第4行使用了一個靜態的敵機對象作為原型,其中獲取敵機實例的方法getInstance(),其簡單的調用克隆方法得到了一個新的克隆對象(此處省略了一場捕獲代碼),并將其橫坐標重設為傳入的參數,最后返回此克隆對象,這樣我們便可以輕松獲取一駕敵機的克隆實例了
敵機克隆工廠類定義完畢,客戶端代碼就留給讀者自己實踐了。
但需要注意,一定要使用懶加載方式,如此既可以節省內存空間,又可以確保敵機的實例化速度,實現敵機的即時性按需克隆,這樣游戲便再也不會出現卡頓現象了。
3. 復雜對象的克隆?- 深拷貝、淺拷貝
最后,在使用原型模式之前,必須搞清楚深拷貝與淺拷貝這兩個概念,否則會對復雜對象的克隆感到無比困惑
假設,敵機類里有一顆子彈可以發射并擊殺玩家的飛機,那么敵機中則包含一顆實例化好的子彈對象,請參考代碼清單:
public class EnemyPlane implements Cloneable {private Bullet bullet = new Bullet();private int x; // 敵機橫坐標private int y = 0; // 敵機縱坐標// 之后代碼省略……
}
如上代碼,此時如果進行克隆操作,能否將子彈對象一起成功克隆呢?
答案是否定的:
- Java中的變量分為原始類型和引用類型,淺拷貝指只復制原始類型的值。而引用類型也會被拷貝,但是這個操作知識拷貝了引用類型的地址引用(指針),也就是說副本敵機與原型敵機中的子彈是同一顆,因為兩個同樣的地址實際指向的內存對象是同一個bullet對象。
- 需要注意的是,克隆方法中調用父類Objecrt的clone方法進行的是淺拷貝,所以此處的bullet并沒有真正克隆。
public class EnemyPlane implements Cloneable {private Bullet bullet;private int x; // 敵機橫坐標private int y = 0; // 敵機縱坐標public EnemyPlane(int x, Bullet bullet) {this.x = x;this.bullet = bullet;}@Overrideprotected EnemyPlane clone() throws CloneNotSupportedException {EnemyPlane clonePlane = (EnemyPlane) super.clone(); // 克隆出敵機clonePlane.setBullet(this.bullet.clone()); // 對子彈進行深拷貝return clonePlane;}// 之后代碼省略……
}
如上代碼顯示,首先clone方法中依舊對敵機對象進行克隆操作,緊接著對敵機子彈bullet也進行了克隆,這個就是深拷貝操作。當然,此處要注意對于子彈類Bullet同樣也得實現克隆接口,請讀者自行實現,此處就不再贅述了。
簡而言之:深拷貝會復制對象及其所有嵌套子對象,而淺拷貝只復制對象本身,嵌套子對象仍然引用原對象。
4. 克隆的本質
在使用克隆模式對游戲代碼反復重構后,游戲性能得到了極大的提升,流暢的游戲畫面確保了優秀的用戶體驗。最后,我們來看原型模式的類結構。
- Prototype(原型接口)?:聲明克隆方法,對應本例程代碼中的Cloneable接口。
- ConcretePrototype(原型實現)?:原型接口的實現類,實現方法中調用super. clone()即可得到新克隆的對象。
- Client(客戶端)?:客戶端只需調用實現此接口的原型對象方法clone(),便可輕松地得到一個全新的實例對象。
從類到對象叫作“創建”?,而由本體對象至副本對象則叫作“克隆”?,當需要創建多個類似的復雜對象時,我們就可以考慮用原型模式。
究其本質,克隆操作時Java虛擬機會進行內存操作,直接拷貝原型對象數據流生成新的副本對象,絕不會拖泥帶水觸發一些多余的復雜操作(如類加載、實例化、初始化等),所以其效率遠遠高于“new”關鍵字所觸發的實例化操作。
-- 秒懂設計模式學習筆記
-- 原型