設計之禪——享元模式

引言

之前已經寫了17篇關於設計模式的文章,而這些設計模式大都是爲了降低代碼之間的耦合,避免違反開閉原則,但它們大都有同樣的一個缺點,產生更多的類和對象,如果數量達到一定程度,就會導致系統性能降低,而今天要講的這個模式就是爲了解決這樣的一個問題,它就是享元模式。

正文

定義

享元模式是爲了儘可能地劃分出細粒度對象並複用,減少內存資源的佔用,因而它是一種優化系統性能的模式。

爲什麼是細粒度的對象複用呢?因爲對象越大,對象的複用率並不高(在系統運行時你不可能總是會使用一些很大的對象),如果將其緩存下來,對系統而言反而是負擔。並且有可能那些特大的對象是我們自己因爲方便而創造出來的,承擔了很多的責任,而你常用的可能只是其中的一部分功能,所以我們需要儘可能的劃分出細粒度對象並緩存起來。因其輕量級特性,享元模式又稱爲蠅量模式

好吧,那照你這麼說,我只需要緩存對象就行了,難道這就是享元模式麼?

當然不是。將對象細粒度劃分後,有其固有不變的屬性,也有些屬性可能會隨着環境的改變而改變,如果將這些屬性都放到一個對象裏,那還怎麼複用這個對象呢?享元模式則是將屬性進行劃分,固有屬性放在對象內部,稱爲內在屬性,而會變化的部分則放在對象的外部,稱爲外在屬性,並通過方法參數傳遞進去,這樣我們就能複用這個對象了。

你這麼說我好像懂了,但還是很模糊。

是的,概念總是抽象的,我舉個例子。比如你有一張圖片,需要顯示在電腦桌面的不同位置,那麼只需要將座標位置屬性抽離出來,根據你的設置動態顯示就行了,這裏座標就是外部會變化的屬性。

這樣就很清楚了,那我在網上看到很多博客都說Java中的字符串就是享元模式的應用,你覺得呢?

我也看到了,但個人不太認同這樣的觀點,String只是利用了緩存,並不能說明使用了享元模式,並且字符串的固有屬性和外部屬性是什麼呢?字符串本身內容嗎?那使用享元模式實現就有些多此一舉了。不過不用太糾結這一點,我們的重點是學習享元模式。

那應該如何實現享元模式呢?

我們先來看看其類圖:
在這裏插入圖片描述
從類圖上看享元模式具有三個角色:

  • 抽象享元接口或抽象類
  • 具體享元類(需要共享的對象)
  • 享元工廠

享元模式常常需要配合工廠模式使用,使用工廠是爲了維護一個共享對象池,將對象緩存到該池中,該池一般使用類似Map的鍵值對來實現,並提供一個方法供外部獲取池中對象,如果池中沒有該對象,就新建一個並放入池中。

說了這麼多,快讓我看代碼吧!

好的,當然沒問題。我想大多數人都知道CS這款遊戲,曾經火遍全球。想想看如果每個角色都新建一個對象是不是很浪費內存呢?畢竟它們基本上一樣,只是需要經常切換武器裝備啊。這就可以使用享元來達到節省內存的目的了啊,就像下面這樣:

Coding

public abstract class AbstractPlayer {

    private String weapon;
    protected String mission;


    public void assignWeapon(String weapon) {
        System.out.println("使用武器:" + weapon);
        this.weapon = weapon;
    }

    public void execute() {
        System.out.println("execute mission: " + mission);
    }
}

public class Police extends AbstractPlayer {

    public Police() {
        this.mission = "kill terrorist!";
    }

}

public class Terrorist extends AbstractPlayer {

    public Terrorist() {
        this.mission = "kill police!";
    }
}

首先我創建了一個抽象的玩家角色類,並實現了土匪和警察兩個具體的類,對於這兩類玩家而言,任務是不會改變的:土匪殺警察,警察殺土匪。而對所有角色而言,武器是隨時都會更換的,因此抽離到外部並通過參數傳入進來。而對象的創建工廠如下:

public class PlayerFactory {

    private static Map<String, AbstractPlayer> pool = new HashMap<>();

    public static AbstractPlayer getPlayer(String type) throws Exception {
        AbstractPlayer player = pool.get(type);
        if (player == null) {
            switch (type) {
                case "P":
                    System.out.println("Create police: ");
                    player = new Police();
                    break;
                case "T":
                    System.out.println("Create terrorist: ");
                    player = new Terrorist();
                    break;
                default:
                    throw new Exception("無此類型的玩家!");
            }
        }
        return player;
    }

}

很簡單,玩家調用getPlayer方法並傳入對應的標識創建警察或是土匪。

public class CS {

    // 角色表示
    private static String[] players = {"T", "P"};
    // 武器類型
    private static String[] weapons = {"AK-47", "Knife", "Sniper"};

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            AbstractPlayer player = PlayerFactory.getPlayer(getPlayer(i));
            player.assignWeapon(getWeapon(i));

            player.execute();
        }
    }

    /**
     * 隨機創建土匪或是警察
     */
    private static String getPlayer(int i) {
        return players[i % players.length];
    }

    /**
     * 隨機獲取武器
     */
    private static String getWeapon(int i) {
        return weapons[i % weapons.length];
    }

}

這裏看起來實現非常簡單,但在實際開發中,要精確把控區分可共享域和不可共享域是非常難的,並且這裏只見到了單純享元模式,另外還有複合享元模式,筆者不再打算展開講述,感興趣的可自行查閱資料。

總結

享元模式在提高對象複用性,節省內存方面非常有用,但在複用度不高時,並不建議使用享元模式,因爲享元工廠本身需要維護一個共享池,浪費資源。並且,需要將變化屬性外部化,使得程序邏輯變得複雜難以理解,同時,外部屬性也會導致運行時間變長。

參考

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