文章目錄
- 一、享元模式的介紹
- 二、實例分析
- 三、示例代碼
一、享元模式的介紹
??享元模式(Flyweight Pattern) 是一種結構型設計模式。通過共享相同對象,減少內存消耗,提高性能。 它摒棄了在每個對象中保存所有數據的方式, 通過共享多個對象所共有的相同狀態, 讓你能在有限的內存容量中載入更多對象。
適用場景:
當系統中存在大量相似對象時,每個對象包含許多重復的狀態(數據),這會浪費大量內存。
享元模式把對象的狀態分為兩類:
- 內部狀態(Intrinsic State):可共享的,不隨環境變化的部分。
- 外部狀態(Extrinsic State):不能共享的,依賴于具體環境的部分。
通過將內部狀態共享,只在需要時傳入外部狀態,即可避免創建大量對象。
享元模式的角色:
- Flyweight(享元接口):定義可以共享的接口,聲明接收外部狀態的方法。
- ConcreteFlyweight(具體享元類):實現享元接口,存儲內部狀態。
- FlyweightFactory(享元工廠類):管理享元對象的創建和復用。
- Client(客戶端):負責傳入外部狀態,使用享元對象。
享元模式結構:
二、實例分析
問題:
假如你希望在長時間工作后放松一下, 所以開發了一款簡單的游戲: 玩家們在地圖上移動并相互射擊。 你決定實現一個真實的粒子系統, 并將其作為游戲的特色。 大量的子彈、 導彈和爆炸彈片會在整個地圖上穿行, 為玩家提供緊張刺激的游戲體驗。
開發完成后, 你推送提交了最新版本的程序, 并在編譯游戲后將其發送給了一個朋友進行測試。 盡管該游戲在你的電腦上完美運行, 但是你的朋友卻無法長時間進行游戲: 游戲總是會在他的電腦上運行幾分鐘后崩潰。 在研究了幾個小時的調試消息記錄后, 你發現導致游戲崩潰的原因是內存容量不足。 朋友的設備性能遠比不上你的電腦, 因此游戲運行在他的電腦上時很快就會出現問題。
真正的問題與粒子系統有關。 每個粒子 (一顆子彈、 一枚導彈或一塊彈片) 都由包含完整數據的獨立對象來表示。 當玩家在游戲中鏖戰進入高潮后的某一時刻, 游戲將無法在剩余內存中載入新建粒子, 于是程序就崩潰了。
解決方案:
仔細觀察粒子Particle類, 你可能會注意到顏色 (color) 和精靈圖 (sprite)這兩個成員變量所消耗的內存要比其他變量多得多。 更糟糕的是, 對于所有的粒子來說, 這兩個成員變量所存儲的數據幾乎完全一樣 (比如所有子彈的顏色和精靈圖都一樣)。
每個粒子的另一些狀態 (坐標、 移動矢量和速度) 則是不同的。 因為這些成員變量的數值會不斷變化。 這些數據代表粒子在存續期間不斷變化的情景, 但每個粒子的顏色和精靈圖則會保持不變。
對象的常量數據通常被稱為內在狀態, 其位于對象中, 其他對象只能讀取但不能修改其數值。 而對象的其他狀態常常能被其他對象 “從外部” 改變, 因此被稱為外在狀態。
享元模式建議不在對象中存儲外在狀態, 而是將其傳遞給依賴于它的一個特殊方法。 程序只在對象中保存內在狀態, 以方便在不同情景下重用。 這些對象的區別僅在于其內在狀態 (與外在狀態相比, 內在狀態的變體要少很多), 因此你所需的對象數量會大大削減。
讓我們回到游戲中。 假如能從粒子類中抽出外在狀態, 那么我們只需三個不同的對象 (子彈、 導彈和彈片) 就能表示游戲中的所有粒子。 你現在很可能已經猜到了, 我們將這樣一個僅存儲內在狀態的對象稱為享元。
外在狀態存儲:
那么外在狀態會被移動到什么地方呢? 總得有類來存儲它們, 對不對? 在大部分情況中, 它們會被移動到容器對象中, 也就是我們應用享元模式前的聚合對象中。
在我們的例子中, 容器對象就是主要的游戲Game對象, 其會將所有粒子存儲在名為 粒子particles的成員變量中。 為了能將外在狀態移動到這個類中, 你需要創建多個數組成員變量來存儲每個粒子的坐標、 方向矢量和速度。 除此之外, 你還需要另一個數組來存儲指向代表粒子的特定享元的引用。 這些數組必須保持同步, 這樣你才能夠使用同一索引來獲取關于某個粒子的所有數據。
更優雅的解決方案是創建獨立的情景類來存儲外在狀態和對享元對象的引用。 在該方法中, 容器類只需包含一個數組。
稍等! 這樣的話情景對象數量不是會和不采用該模式時的對象數量一樣多嗎? 的確如此, 但這些對象要比之前小很多。 消耗內存最多的成員變量已經被移動到很少的幾個享元對象中了。 現在, 一個享元大對象會被上千個情境小對象復用, 因此無需再重復存儲數千個大對象的數據。
享元與不可變性:
由于享元對象可在不同的情景中使用, 你必須確保其狀態不能被修改。 享元類的狀態只能由構造函數的參數進行一次性初始化, 它不能對其他對象公開其設置器或公有成員變量。
享元工廠:
為了能更方便地訪問各種享元, 你可以創建一個工廠方法來管理已有享元對象的緩存池。 工廠方法從客戶端處接收目標享元對象的內在狀態作為參數, 如果它能在緩存池中找到所需享元, 則將其返回給客戶端; 如果沒有找到, 它就會新建一個享元, 并將其添加到緩存池中。
你可以選擇在程序的不同地方放入該函數。 最簡單的選擇就是將其放置在享元容器中。 除此之外, 你還可以新建一個工廠類, 或者創建一個靜態的工廠方法并將其放入實際的享元類中。
三、示例代碼
示例一:
在一個文本編輯器里,每個字符都是一個對象,如果每個字符都單獨創建,就會消耗大量內存。實際上,每個字符的字形(內部狀態) 是可以共享的,只需要保存它們在文檔中的位置、大小、顏色等外部狀態 即可。
#include <iostream>
#include <map>
#include <string>
using namespace std;// 享元接口
class Character {
public:virtual void display(int size, int x, int y) = 0; // 外部狀態:字體大小、位置virtual ~Character() {}
};// 具體享元類:具體字符
class ConcreteCharacter : public Character {
private:char symbol; // 內部狀態:字符本身
public:ConcreteCharacter(char c) : symbol(c) {}void display(int size, int x, int y) override {cout << "字符: " << symbol<< " 字體大小: " << size<< " 位置: (" << x << "," << y << ")\n";}
};// 享元工廠類:管理共享對象
class CharacterFactory {
private:map<char, Character*> pool; // 享元池
public:~CharacterFactory() {for (auto& kv : pool) delete kv.second;}Character* getCharacter(char c) {if (pool.find(c) == pool.end()) {pool[c] = new ConcreteCharacter(c);}return pool[c];}
};// 客戶端
int main() {CharacterFactory factory;Character* c1 = factory.getCharacter('A');Character* c2 = factory.getCharacter('B');Character* c3 = factory.getCharacter('A'); // 復用已有對象// 顯示字符(外部狀態由客戶端傳入)c1->display(12, 10, 20);c2->display(14, 15, 25);c3->display(16, 50, 100);return 0;
}
注意:雖然創建了三個字符,但實際上 ‘A’ 只創建了一次,第二次復用,節省了內存。
示例二:
使用用五子棋(棋盤游戲)舉例實現享元模式
場景分析:
- 在五子棋中:棋子分為黑子和白子兩種。
- 每顆棋子的顏色可以共享(內部狀態 Intrinsic State)。
- 棋子的位置 (x, y) 每次都不同(外部狀態 Extrinsic State)。
因此,整個棋盤上無論下多少顆棋子,實際上只需要兩個享元對象(黑子、白子)。
#include <iostream>
#include <map>
#include <string>
using namespace std;// 抽象享元類:棋子
class ChessPiece {
public:virtual void draw(int x, int y) = 0; // 外部狀態:棋子位置virtual ~ChessPiece() {}
};// 具體享元類:黑子
class BlackPiece : public ChessPiece {
public:void draw(int x, int y) override {cout << "黑子落在位置 (" << x << "," << y << ")\n";}
};// 具體享元類:白子
class WhitePiece : public ChessPiece {
public:void draw(int x, int y) override {cout << "白子落在位置 (" << x << "," << y << ")\n";}
};// 享元工廠:管理棋子對象
class PieceFactory {
private:map<string, ChessPiece*> pool; // 棋子池
public:~PieceFactory() {for (auto& kv : pool) delete kv.second;}ChessPiece* getPiece(const string& color) {if (pool.find(color) == pool.end()) {if (color == "black") {pool[color] = new BlackPiece();} else if (color == "white") {pool[color] = new WhitePiece();}}return pool[color];}
};// 客戶端:模擬下棋
int main() {PieceFactory factory;ChessPiece* black = factory.getPiece("black");ChessPiece* white = factory.getPiece("white");// 下棋(外部狀態:位置由客戶端傳入)black->draw(3, 3);white->draw(4, 4);black->draw(5, 5);white->draw(6, 6);black->draw(7, 7);return 0;
}