享元模式:“享”就是分享之意,指一物被衆人共享,而這也正是該模式的終旨所在。
享元模式有點類似於單例模式,都是隻生成一個對象來被共享使用。這裏有個問題,那就是對共享對象的修改,爲了避免出現這種情況,我們將這些對象的公共部分,或者說是不變化的部分抽取出來形成一個對象。這個對象就可以避免到修改的問題。
享元的目的是爲了減少不會要額內存消耗,將多個對同一對象的訪問集中起來,不必爲每個訪問者創建一個單獨的對象,以此來降低內存的消耗。
舉個例子,例如我的世界中
有各種各樣,有草地、沙漠、荒原,水路等等,在寫代碼之前,我們先思考下應該怎樣去建模。
對於這種地圖,我們加載一整張圖片來做地圖?如果地圖太大,圖片加載相當卡頓吧?而且大片地圖上其實都是重複的圖片素材,整圖加載設計也有失靈活性。再仔細觀察下,這地圖無非就是很多小圖片(元)拼起來的哦,這不就是類似於我們裝修時貼馬賽克嘛?
這可簡單了!我們應該有個磚塊類,持有“圖片”,“位置”等屬性信息,然後實例化這些磚塊再調用其“繪製”方法把圖片顯示在地圖某位置上即可。二話不說開始寫代碼。
package com.yitian.observer;
/**
* @Description TODO
* @Author yitianRen
* @Date 2019/9/24 16:16
* @Version 1.0
**/
public class Tile {
private String image;//地磚所用的圖片材質
private int x, y;//地磚所在座標
public Tile(String image, int x, int y) {
this.image = image;
System.out.print("從磁盤加載[" + image + "]圖片,耗時半秒。。。");
this.x = x;
this.y = y;
}
public void draw() {
System.out.println("在位置[" + x + ":" + y + "]上繪製圖片:[" + image + "]");
}
}
運行一下:
package com.yitian.observer;
/**
* @Description TODO
* @Author yitianRen
* @Date 2019/9/24 16:18
* @Version 1.0
**/
public class Client {
public static void main(String[] args) {
//以繪製第一行爲例
new Tile("河流", 10, 10).draw();
new Tile("河流", 10, 20).draw();
new Tile("石路", 10, 30).draw();
new Tile("草坪", 10, 40).draw();
new Tile("草坪", 10, 50).draw();
new Tile("草坪", 10, 60).draw();
new Tile("草坪", 10, 70).draw();
new Tile("草坪", 10, 80).draw();
/* 運行結果
從磁盤加載[河流]圖片,耗時半秒。。。在位置[10:10]上繪製圖片:[河流]
從磁盤加載[河流]圖片,耗時半秒。。。在位置[10:20]上繪製圖片:[河流]
從磁盤加載[石路]圖片,耗時半秒。。。在位置[10:30]上繪製圖片:[石路]
從磁盤加載[草坪]圖片,耗時半秒。。。在位置[10:40]上繪製圖片:[草坪]
從磁盤加載[草坪]圖片,耗時半秒。。。在位置[10:50]上繪製圖片:[草坪]
從磁盤加載[草坪]圖片,耗時半秒。。。在位置[10:60]上繪製圖片:[草坪]
從磁盤加載[草坪]圖片,耗時半秒。。。在位置[10:70]上繪製圖片:[草坪]
從磁盤加載[草坪]圖片,耗時半秒。。。在位置[10:80]上繪製圖片:[草坪]
*/
}
}
有沒有發現問題?每加載一張圖都要耗費掉半秒鐘,才畫了8張地磚圖就4秒鐘流逝了,如果構建整張地圖得多少時間?這就像是在慢性自殺,如此效率嚴重影響了遊戲的用戶體驗,光卡頓在地圖加載這給漫長的過程就已經讓玩家失去興趣了。
相信大家一定想到了《設計模式是什麼鬼(原型)》模式吧?對,我們把相同的圖共享出來,用克隆的方式代替物件圖實例化的過程,從而加快初始化速度。再想想,共享元貌似沒什麼問題,速度也加快了,但對象數量貌似還是個嚴重問題,每一個小物件圖都要對應一個對象,這麼個小遊戲用得着那麼大的內存開銷麼,搞不好甚至會造成內存溢出,嗯,設計模式一定還是有問題。
沿着共享的思路我們再看下到底需不需要這麼多對象?這些對象不同的地方在於其座標的不同,再就是材質的不同,也就是圖的不同了,能不能從這些對象裏抽取出來一些共同點呢?首先每個圖的座標都不一樣,是沒辦法共享的,但是材質圖是重複出現的,是可以共享的,同樣的材質圖會在不同的座標位置上重複出現,那麼這個材質圖是可以做成共享元的。
既然座標不能共享,那就不做爲材質類的共享元屬性,由客戶端維護這些座標並作爲參數傳入好了,而且這些材質都有繪製能力,那就先定義一個接口吧。
package com.yitian.observer;
public interface Coordinate {
void draw(int x, int y);//繪製方法,接收地圖座標。
}
當然,我們也可以用抽象類抽出更多的屬性和方法代替接口,使子類變得簡單,這裏爲了清晰說明問題就用接口。接下來是材質類們,統統實現這個繪製接口。
package com.yitian.observer;
/**
* @Description TODO
* @Author yitianRen
* @Date 2019/9/24 16:48
* @Version 1.0
**/
public class Grass implements Coordinate {
private String image;//草坪圖片材質
public Grass() {
this.image = "草坪";
System.out.print("從磁盤加載[" + image + "]圖片,耗時半秒。。。");
}
@Override
public void draw(int x, int y) {
System.out.println("在位置[" + x + ":" + y + "]上繪製圖片:[" + image + "]");
}
}
package com.yitian.observer;
/**
* @Description TODO
* @Author yitianRen
* @Date 2019/9/24 17:01
* @Version 1.0
**/
public class House implements Coordinate {
private String image;//房子圖片材質
public House() {
this.image = "房子";
System.out.print("從磁盤加載[" + image + "]圖片,耗時一秒。。。");
}
@Override
public void draw(int x, int y) {
System.out.println("將圖層切到最上層。。。");//房子蓋在地上,所以切換到頂層圖層。
System.out.println("在位置[" + x + ":" + y + "]上繪製圖片:[" + image + "]");
}
}
package com.yitian.observer;
/**
* @Description TODO
* @Author yitianRen
* @Date 2019/9/24 16:59
* @Version 1.0
**/
public class Stone implements Coordinate {
private String image;//石路圖片材質
public Stone() {
this.image = "石路";
System.out.print("從磁盤加載[" + image + "]圖片,耗時半秒。。。");
}
@Override
public void draw(int x, int y) {
System.out.println("在位置[" + x + ":" + y + "]上繪製圖片:[" + image + "]");
}
}
package com.yitian.observer;
/**
* @Description TODO
* @Author yitianRen
* @Date 2019/9/24 16:45
* @Version 1.0
**/
public class Water implements Coordinate {
private String image;//河流圖片材質
public Water() {
this.image = "河流";
System.out.print("從磁盤加載[" + image + "]圖片,耗時半秒。。。");
}
@Override
public void draw(int x, int y) {
System.out.println("在位置[" + x + ":" + y + "]上繪製圖片:[" + image + "]");
}
}
接下來就是實現“元之共享”的關鍵了,我們來做一個簡單工廠類,看代碼。
package com.yitian.observer;
import java.util.HashMap;
/**
* @Description TODO
* @Author yitianRen
* @Date 2019/9/24 17:07
* @Version 1.0
**/
public class CoordinateFactory {
private HashMap<String, Coordinate> images;//圖庫
public CoordinateFactory() {
this.images = new HashMap<>();
}
public Coordinate getCoordinate(String image) {
if (!images.containsKey(image)) {
//緩存裏如果沒有圖件,則實例化並放入緩存。
switch (image) {
case "河流":
images.put(image, new Water());
break;
case "草坪":
images.put(image, new Grass());
break;
case "石路":
images.put(image, new Stone());
}
}
//緩存裏必然有圖,直接取得並返回。
return images.get(image);
}
}
這個圖件工廠維護着所有元對象的圖庫,構造方法於會初始化一個哈希圖的緩存”池“,當客戶端於需要實例化圖件的時候,我們先觀察這個圖庫池裏存在不存在已實例化過的圖件,也就是看有無已做共享的圖元,如果沒有則實例化並加入圖庫共享池供下次使用,這便是”元之共享“的祕密了。巧奪天工的設計一氣呵成,已經迫不及待去運行了。
package com.yitian.observer;
/**
* @Description TODO
* @Author yitianRen
* @Date 2019/9/24 17:22
* @Version 1.0
**/
public class CoordinateClient {
public static void main(String[] args) {
//先實例化圖件工廠
CoordinateFactory factory = new CoordinateFactory();
//以第一行爲例
factory.getCoordinate("河流").draw(10, 10);
factory.getCoordinate("河流").draw(10, 20);
factory.getCoordinate("石路").draw(10, 30);
factory.getCoordinate("草坪").draw(10, 40);
factory.getCoordinate("草坪").draw(10, 50);
factory.getCoordinate("草坪").draw(10, 60);
factory.getCoordinate("草坪").draw(10, 70);
factory.getCoordinate("草坪").draw(10, 80);
/*運行結果
從磁盤加載[河流]圖片,耗時半秒。。。在位置[10:10]上繪製圖片:[河流]
在位置[10:20]上繪製圖片:[河流]
從磁盤加載[石路]圖片,耗時半秒。。。在位置[10:30]上繪製圖片:[石路]
從磁盤加載[草坪]圖片,耗時半秒。。。在位置[10:40]上繪製圖片:[草坪]
在位置[10:50]上繪製圖片:[草坪]
在位置[10:60]上繪製圖片:[草坪]
在位置[10:70]上繪製圖片:[草坪]
在位置[10:80]上繪製圖片:[草坪]
*/
}
}
可以看到,我們拋棄了利用new關鍵字肆意妄爲地製造對象,而是改用這個圖件工廠去幫我們把元構建並共享起來。顯而易見,我們看到運行結果中每次實例化對象會耗費半秒時間,再次請求對象時就不再會加載圖片耗費時間了,也就是從共享圖池直接拿到了,不再造次。更妙的是,如果畫完整個地圖只需要實例化需要用到的某些元素材而已,即使是那個大房子圖件也只需要實例化一次就夠了。至此,CPU速度,內存輕量化同時做到了優化,整個遊戲用戶體驗得到了極大的提升。
享元的精髓當然重點不止於”享“,更重要的是對於元的辨識,例如那個從外部客戶端傳入的座標參數,如果我們依然把座標也當作共享對象元數據(內蘊狀態)的話,那麼這個結構將無元可享,大量的對象就如同世界上沒有相同的兩片樹葉一樣多不勝數,最終會導致圖庫池被撐爆,享元將變得毫無意義。所以,對於整個系統數據結構的分析、設計、規劃顯得尤爲重要。
文章來源 微信公衆號 java知音 設計模式