引言
之前已經寫了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];
}
}
這裏看起來實現非常簡單,但在實際開發中,要精確把控區分可共享域和不可共享域是非常難的,並且這裏只見到了單純享元模式,另外還有複合享元模式,筆者不再打算展開講述,感興趣的可自行查閱資料。
總結
享元模式在提高對象複用性,節省內存方面非常有用,但在複用度不高時,並不建議使用享元模式,因爲享元工廠本身需要維護一個共享池,浪費資源。並且,需要將變化屬性外部化,使得程序邏輯變得複雜難以理解,同時,外部屬性也會導致運行時間變長。