設計模式之享元模式

享元模式:“享”就是分享之意,指一物被衆人共享,而這也正是該模式的終旨所在。

  享元模式有點類似於單例模式,都是隻生成一個對象來被共享使用。這裏有個問題,那就是對共享對象的修改,爲了避免出現這種情況,我們將這些對象的公共部分,或者說是不變化的部分抽取出來形成一個對象。這個對象就可以避免到修改的問題。

  享元的目的是爲了減少不會要額內存消耗,將多個對同一對象的訪問集中起來,不必爲每個訪問者創建一個單獨的對象,以此來降低內存的消耗。

舉個例子,例如我的世界中

有各種各樣,有草地、沙漠、荒原,水路等等,在寫代碼之前,我們先思考下應該怎樣去建模。

對於這種地圖,我們加載一整張圖片來做地圖?如果地圖太大,圖片加載相當卡頓吧?而且大片地圖上其實都是重複的圖片素材,整圖加載設計也有失靈活性。再仔細觀察下,這地圖無非就是很多小圖片(元)拼起來的哦,這不就是類似於我們裝修時貼馬賽克嘛?

這可簡單了!我們應該有個磚塊類,持有“圖片”,“位置”等屬性信息,然後實例化這些磚塊再調用其“繪製”方法把圖片顯示在地圖某位置上即可。二話不說開始寫代碼。

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知音  設計模式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章