文章目錄
- 簡介
- 問題
- 解決方案
- 享元與不可變性
- 享元工廠
- 代碼
- 總結
簡介
亦稱:緩存、Cache、Flyweight。享元是一種結構型設計模式,它摒棄了在每個對象中保存所有數據的方式,通過共享多個對象所共有的相同狀態,讓你能在有限的內存容量中載入更多對象。
問題
假如你開發了一款簡單的游戲:玩家可以在地圖上移動并相互射擊,比如刺激戰場。你決定實現一個真實的粒子系統。大量的子彈、導彈和爆炸彈片會在整個地圖上穿行,給玩家提供刺激的游戲體驗。
開發完成后,你編譯好打包然后發送給了一個朋友進行測試。盡管這個游戲在你的電腦上正常運行,但是你的朋友卻無法長時間進行游戲:游戲總是會在他的電腦上運行幾分鐘后崩潰。在研究了調試信息后,你發現導致游戲崩潰的原因是電腦內存容量不足。朋友的設備性能遠比不上你的電腦,所以游戲運行之后很快就會出現問題。
真正的問題其實是跟粒子系統有關。每個粒子(一顆子彈、一枚導彈或一塊彈片)都由包含完整數據的獨立對象來表示,如下圖,每個粒子占用約21KB內存。當玩家打到高潮的時候(粒子數大約有1000000),會占用大約21GB的內存,這時內存已經不能再容納新粒子了,于是程序就崩潰了。
解決方案
仔細觀察粒子Par-ti-cle類,你可能會注意到顏色(color)和紋理圖(sprite)這兩個成員變量所消耗的內存要比其他變量多得多。而且對于所有的粒子來說,這兩個成員變量所存儲的數據幾乎完全一樣(比如所有子彈的顏色和紋理圖都一樣),如下圖(Particle)。
每個粒子的另一些狀態(坐標、移動矢量和速度)則是不同的(MovingParticle)。因為這些成員變量的數值會不斷變化。這些數據能夠代表粒子在創建之后不斷變化的情景,但每個粒子的顏色和紋理圖其實會保持不變。
我們知道,對象的常量數據通常被稱為內在狀態,它在對象里面,其他對象只能讀取但不能修改他的數值。而對象的其他狀態常常能被其他對象“從外部”改變,因此被稱為外在狀態。
享元模式建議不在對象中存儲外在狀態,而是把他傳遞給依賴于它的一個特殊方法。程序只在對象里保存內在狀態,以方便在不同情景下重用。這些對象的區別僅在于他的內在狀態(和外在狀態相比,內在狀態的變化要少很多),因此你所需的對象數量會大大削減。
讓我們回到游戲的示例。假如能從粒子類里抽出外在狀態(MovingParticle),那么我們只需要三個不同的對象(子彈、導彈和彈片)就能表示游戲中的所有粒子。如下圖,我們把MovingParticle從Particle中抽出來之后,只要一個Particle對象存儲顏色和紋理圖(21KB)即可, 其余外在狀態都存儲在MovingParticle對象中(32B),MovingParticle 對象會共享這一個Particle對象。你現在很可能已經猜到了,我們把這樣一個僅存儲內在狀態(Particle)的對象稱為享元。
享元與不可變性
由于享元對象可以在不同的情景中使用,你必須確保他的狀態不能被修改。享元類的狀態只能由構造函數的參數進行一次性初始化,它不能對其他對象公開他的setter或公有成員變量。
享元工廠
為了能更方便地訪問各種享元,你可以創建一個工廠方法來管理已有享元對象的緩存池。工廠方法從客戶端處接收目標享元對象的內在狀態作為參數,如果它能在緩存池中找到所需享元,就把它返回給客戶端;如果沒有找到,它就會新建一個享元,并把他添加到緩存池里。
你可以選擇在程序的不同地方放入這個函數。最簡單的選擇就是把他放在享元容器里。除此之外,你還可以新建一個工廠類,或者創建一個靜態的工廠方法并把它放在實際的享元類里。
代碼
// 粒子內在狀態
final class ParticleType {private final String color; // 不可變特征private final String texture;private final String effectType;public ParticleType(String color, String texture, String effectType) {this.color = color;this.texture = texture;this.effectType = effectType;}public void render(String position, double velocity) {System.out.printf("繪制%s特效: 位置%s | 速度%.1fm/s | 材質[%s]%n",effectType, position, velocity, texture);}
}// 粒子外在狀態載體
class Particle {private double x, y;private double velocity;private final ParticleType type; // 共享引用public Particle(double x, double y, double v, ParticleType type) {this.x = x;this.y = y;this.velocity = v;this.type = type;}public void display() {type.render(String.format("(%.1f, %.1f)", x, y), velocity);}
}// 享元工廠
class ParticleFactory {private static final Map<String, ParticleType> pool = new HashMap<>();public static ParticleType getType(String color, String texture, String effect) {String key = color + "_" + texture + "_" + effect;// 池化檢測邏輯if (!pool.containsKey(key)) {pool.put(key, new ParticleType(color, texture, effect));}return pool.get(key);}
}// 粒子系統管理
class ParticleSystem {private List<Particle> particles = new ArrayList<>();public void addParticle(double x, double y, double v,String color, String texture, String effect) {ParticleType type = ParticleFactory.getType(color, texture, effect);particles.add(new Particle(x, y, v, type));}public void simulate() {particles.forEach(Particle::display);}
}// 運行示例
class GameEngine {public static void main(String[] args) {ParticleSystem system = new ParticleSystem();// 添加火焰粒子system.addParticle(10.5, 20.3, 5.2, "橙紅", "fire_tex", "火焰");system.addParticle(15.1, 18.7, 4.8, "橙紅", "fire_tex", "火焰");// 添加冰雪粒子system.addParticle(30.0, 50.0, 2.1, "冰藍", "snow_tex", "冰霜");system.simulate();}
}
總結
- 享元模式只是一種優化。在應用這個模式之前,你要確定程序里存在內存消耗問題,并且這個問題是跟大量類似對象同時占用內存相關的,同時確保這個問題沒辦法用其他更好的方式來解決。
- 享元(Fly-weight)類包含原始對象里部分能在多個對象中共享的狀態。同一享元對象可以在許多不同情景中使用。享元中存儲的狀態被稱為“內在狀態”。
- 情景(Con-text)類包含原始對象里各不相同的外在狀態。情景和享元對象組合在一起就能表示原始對象的全部狀態。
- 通常情況下,原始對象的行為會保留在享元類中。因此調用享元的方法必須提供部分外在狀態作為參數。但你也可把行為移動到情景類里,然后把連入的享元作為單純的數據對象。
- 客戶端(Client)負責計算或存儲享元的外在狀態。在客戶端看來,享元是一種可以在運行時進行配置的模板對象,具體的配置方式就是向他的方法里面傳入一些情景數據參數。
- 享元工廠(Fly-weight Fac-to-ry)會對已有享元的緩存池進行管理。有了工廠后,客戶端就無需直接創建享元,它們只需調用工廠并且向他傳遞目標享元的一些內在狀態就可以了。工廠會根據參數在之前已創建的享元中進行查找,如果找到滿足條件的享元就直接返回;如果沒有找到就根據參數新建享元。