設計模式是什麼鬼(享元)
//作者:凸凹裡歐
元,始也,本初,根源之意,計算機中的二進位制“元”其實就1和0,這兩個東西組合起來有無窮無盡的可能,這便形成了計算機中的大千世界,正如“陰”和“陽”為萬物之首一樣,這也是為什麼稱其為二元。顧名思義,享元就是共享本元的意思,然而這個模式的英文叫做Flyweight,能飛起來一般的重量,輕量級的意思,“享元”其實並非意譯,但這並不影響其對這個模式的最佳詮釋。
我們來看一個例項,比如我們要開發一款RPG遊戲,遊戲地圖通常非常大,而且有各種各樣,有草地、沙漠、荒原,水路等等,在寫程式碼之前,我們先思考下應該怎樣去建模。
對於這種地圖,我們載入一整張圖片來做地圖?如果地圖太大,圖片載入相當卡頓吧?而且大片地圖上其實都是重複的圖片素材,整圖載入設計也有失靈活性。再仔細觀察下,這地圖無非就是很多小圖片(元)拼起來的哦,這不就是類似於我們裝修時貼馬賽克嘛?
這可簡單了!我們應該有個磚塊類,持有“圖片”,“位置”等屬性資訊,然後例項化這些磚塊再呼叫其“繪製”方法把圖片顯示在地圖某位置上即可。二話不說開始寫程式碼。
1 public class Tile { 2private String image;//地磚所用的圖片材質 3private int x, y;//地磚所在座標 4public Tile(String image, int x, int y) { 5this.image = image; 6System.out.print("從磁碟載入[" + image + "]圖片,耗時半秒。。。"); 7this.x = x; 8this.y = y; 9} 10public void draw() { 11System.out.println("在位置[" + x + ":" + y + "]上繪製圖片:[" + image + "]"); 12} 13 }
程式碼看起來非常簡單,第3行的地磚材質圖片我們用String來模擬代替,第7行初始化時我們把圖片載入到記憶體,比如說這個IO操作要耗費半秒時間,好了我們先測試繪製第一行磚塊,執行一下。
1 public class Client { 2public static void main(String[] args) { 3//以繪製第一行為例 4new Tile("河流", 10, 10).draw(); 5new Tile("河流", 10, 20).draw(); 6new Tile("石路", 10, 30).draw(); 7new Tile("草坪", 10, 40).draw(); 8new Tile("草坪", 10, 50).draw(); 9new Tile("草坪", 10, 60).draw(); 10new Tile("草坪", 10, 70).draw(); 11new Tile("草坪", 10, 80).draw(); 12/* 執行結果 13從磁碟載入[河流]圖片,耗時半秒。。。在位置[10:10]上繪製圖片:[河流] 14從磁碟載入[河流]圖片,耗時半秒。。。在位置[10:20]上繪製圖片:[河流] 15從磁碟載入[石路]圖片,耗時半秒。。。在位置[10:30]上繪製圖片:[石路] 16從磁碟載入[草坪]圖片,耗時半秒。。。在位置[10:40]上繪製圖片:[草坪] 17從磁碟載入[草坪]圖片,耗時半秒。。。在位置[10:50]上繪製圖片:[草坪] 18從磁碟載入[草坪]圖片,耗時半秒。。。在位置[10:60]上繪製圖片:[草坪] 19從磁碟載入[草坪]圖片,耗時半秒。。。在位置[10:70]上繪製圖片:[草坪] 20從磁碟載入[草坪]圖片,耗時半秒。。。在位置[10:80]上繪製圖片:[草坪] 21*/ 22} 23 }
有沒有發現問題?每載入一張圖都要耗費掉半秒鐘,才畫了8張地磚圖就4秒鐘流逝了,如果構建整張地圖得多少時間?這就像是在慢性自殺,如此效率嚴重影響了遊戲的使用者體驗,光卡頓在地圖載入這給漫長的過程就已經讓玩家失去興趣了。
相信大家一定想到了《設計模式是什麼鬼(原型)》模式吧?對,我們把相同的圖共享出來,用克隆的方式代替物件圖例項化的過程,從而加快初始化速度。再想想,共享元貌似沒什麼問題,速度也加快了,但物件數量貌似還是個嚴重問題,每一個小物件圖都要對應一個物件,這麼個小遊戲用得著那麼大的記憶體開銷麼,搞不好甚至會造成記憶體溢位,嗯,設計模式一定還是有問題。
沿著共享的思路我們再看下到底需不需要這麼多物件?這些物件不同的地方在於其座標的不同,再就是材質的不同,也就是圖的不同了,能不能從這些物件裡抽取出來一些共同點呢?首先每個圖的座標都不一樣,是沒辦法共享的,但是材質圖是重複出現的,是可以共享的,同樣的材質圖會在不同的座標位置上重複出現,那麼這個材質圖是可以做成共享元的。
既然座標不能共享,那就不做為材質類的共享元屬性,由客戶端維護這些座標並作為引數傳入好了,而且這些材質都有繪製能力,那就先定義一個介面吧。
1 public interface Drawable { 2 3 void draw(int x, int y);//繪製方法,接收地圖座標。 4 5 }
當然,我們也可以用抽象類抽出更多的屬性和方法代替介面,使子類變得簡單,這裡為了清晰說明問題就用介面。接下來是材質類們,統統實現這個繪製介面。
1 public class Water implements Drawable { 2 3 private String image;//河流圖片材質 4 5 public Water() { 6 7 this.image = "河流"; 8 9 System.out.print("從磁碟載入[" + image + "]圖片,耗時半秒。。。"); 10 11 } 12 13 @Override 14 15 public void draw(int x, int y) { 16 17 System.out.println("在位置[" + x + ":" + y + "]上繪製圖片:[" + image + "]"); 18 19 } 20 21 }
注意第6行因為是河流材質類,所以初始化我們直接載入河流圖片素材,這就是類內部即將做共享的“元”資料了,也叫做“內蘊狀態”,至於“外蘊狀態”就是座標了,只作為引數從外部傳入不做共享。接下來是草地、石子路等等。
1 public class Grass implements Drawable { 2private String image;//草坪圖片材質 3public Grass() { 4this.image = "草坪"; 5System.out.print("從磁碟載入[" + image + "]圖片,耗時半秒。。。"); 6} 7@Override 8public void draw(int x, int y) { 9System.out.println("在位置[" + x + ":" + y + "]上繪製圖片:[" + image + "]"); 10} 11 }
1 public class Stone implements Drawable { 2private String image;//石路圖片材質 3public Stone() { 4this.image = "石路"; 5System.out.print("從磁碟載入[" + image + "]圖片,耗時半秒。。。"); 6} 7@Override 8public void draw(int x, int y) { 9System.out.println("在位置[" + x + ":" + y + "]上繪製圖片:[" + image + "]"); 10} 11 }
1 public class House implements Drawable { 2private String image;//房子圖片材質 3public House() { 4this.image = "房子"; 5System.out.print("從磁碟載入[" + image + "]圖片,耗時一秒。。。"); 6} 7@Override 8public void draw(int x, int y) { 9System.out.println("將圖層切到最上層。。。");//房子蓋在地上,所以切換到頂層圖層。 10System.out.println("在位置[" + x + ":" + y + "]上繪製圖片:[" + image + "]"); 11} 12 }
注意上面這個的房子類有所不同,它有自己特有的繪製行為方法,也就是在地板圖層之上繪製房子,覆蓋掉下面的地板,使其變得更加立體。這也就是為什麼我們非要用介面或抽象類來做引用,使實現類可以有自己獨特的行為方式,多型的好處立竿見影。接下來就是實現“元之共享”的關鍵了,我們來做一個簡單工廠類,看程式碼。
1public class Factory {//圖件工廠 2private Map<String, Drawable> images;//相簿 3public Factory() { 4images = new HashMap<String, Drawable>(); 5} 6public Drawable getDrawable(String image) { 7//快取裡如果沒有圖件,則例項化並放入快取。 8if(!images.containsKey(image)){ 9switch (image) { 10case "河流": 11images.put(image, new Water()); 12break; 13case "草坪": 14images.put(image, new Grass()); 15break; 16case "石路": 17images.put(image, new Stone()); 18} 19} 20//快取裡必然有圖,直接取得並返回。 21return images.get(image); 22} 23 }
這個圖件工廠維護著所有元物件的相簿,構造方法於第5行會初始化一個雜湊圖的快取”池“,當客戶端於第8行需要例項化圖件的時候,我們先觀察這個相簿池裡存在不存在已例項化過的圖件,也就是看有無已做共享的圖元,如果沒有則例項化並加入相簿共享池供下次使用,這便是”元之共享“的祕密了。巧奪天工的設計一氣呵成,已經迫不及待去運行了。
1 public class Client { 2public static void main(String[] args) { 3//先例項化圖件工廠 4Factory factory = new Factory(); 5//以第一行為例 6factory.getDrawable("河流").draw(10, 10); 7factory.getDrawable("河流").draw(10, 20); 8factory.getDrawable("石路").draw(10, 30); 9factory.getDrawable("草坪").draw(10, 40); 10factory.getDrawable("草坪").draw(10, 50); 11factory.getDrawable("草坪").draw(10, 60); 12factory.getDrawable("草坪").draw(10, 70); 13factory.getDrawable("草坪").draw(10, 80); 14/*執行結果 15從磁碟載入[河流]圖片,耗時半秒。。。在位置[10:10]上繪製圖片:[河流] 16在位置[10:20]上繪製圖片:[河流] 17從磁碟載入[石路]圖片,耗時半秒。。。在位置[10:30]上繪製圖片:[石路] 18從磁碟載入[草坪]圖片,耗時半秒。。。在位置[10:40]上繪製圖片:[草坪] 19在位置[10:50]上繪製圖片:[草坪] 20在位置[10:60]上繪製圖片:[草坪] 21在位置[10:70]上繪製圖片:[草坪] 22在位置[10:80]上繪製圖片:[草坪] 23*/ 24} 25 }
可以看到,我們拋棄了利用new關鍵字肆意妄為地製造物件,而是改用這個圖件工廠去幫我們把元構建並共享起來。顯而易見,我們看到執行結果中每次例項化物件會耗費半秒時間,再次請求物件時就不再會載入圖片耗費時間了,也就是從共享圖池直接拿到了,不再造次。更妙的是,如果畫完整個地圖只需要例項化需要用到的某些元素材而已,即使是那個大房子圖件也只需要例項化一次就夠了。至此,CPU速度,記憶體輕量化同時做到了優化,整個遊戲使用者體驗得到了極大的提升。
享元的精髓當然重點不止於”享“,更重要的是對於元的辨識,例如那個從外部客戶端傳入的座標引數,如果我們依然把座標也當作共享物件元資料(內蘊狀態)的話,那麼這個結構將無元可享,大量的物件就如同世界上沒有相同的兩片樹葉一樣多不勝數,最終會導致相簿池被撐爆,享元將變得毫無意義。所以,對於整個系統資料結構的分析、設計、規劃顯得尤為重要。
內外相濟,裡應外合,以不變應萬變的化繁為簡,元,萬變不離其宗,享之。