FlyWeight 享元模式

一.意图

Flyweight(轻量 级)是一种结构设计模式,通过在多个对象之间共享共享状态部分,而不是将所有数据都放在每个对象中,从而在可用内存中容纳更多对象。(复用内存中已存在的相同对象,减少创建大量相同 / 相似对象的内存开销,避免对象泛滥。)

在软件系统采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价------主要指内存需求方面的代价。

运行共享技术有效地支持大量细粒度的对象。 ------《设计模式》GoF

二.问题

为了在长时间工作后找点乐趣,你决定做一个简单的电子游戏:玩家们在地图上移动并互相射击。你选择实现一个逼真的粒子系统,并将其作为游戏的独特特色。大量子弹、导弹和爆炸碎片散落在地图上,给玩家带来刺激的体验。

完成后,你推送了最后一个提交,构建好游戏,然后发给朋友试驾。虽然游戏在你的电脑上运行得很顺畅,但你的朋友没能玩多久。在他的电脑上,游戏在玩了几分钟后就崩溃了。经过数小时的调试日志,你发现游戏崩溃是因为内存不足。结果发现你朋友的电脑性能远不如你自己的电脑,这也是为什么他电脑上问题会这么快出现。

真正的问题是和你的粒子系统有关。每个粒子,如子弹、导弹或弹片,都由一个包含大量数据的独立物体表示。当玩家屏幕上的混乱达到顶点时,新生成的粒子不再能装进剩余的内存,程序崩溃。

三.解决方案

仔细观察该类,你可能会注意到颜色和Particle精灵字段比其他字段消耗更多的内存。更糟糕的是,这两个场在所有粒子中存储的数据几乎相同。例如,所有子弹的颜色和精灵都相同。

粒子状态的其他部分,如坐标、运动矢量和速度,则是每个粒子独一无二的。毕竟,这些场的数值会随着时间变化。这些数据代表粒子存在的不断变化的上下文,而每个粒子的颜色和精灵保持不变。

物体的这种常数数据通常称为内在状态 。它存在于物体之中;其他对象只能读取,不能更改。物体的其余状态,通常被其他物体"从外部"改变,称为外在状态

蝇量模式建议你停止将外在状态储存在物体内部。相反,你应该把这个状态传递给依赖它的具体方法。只有内在状态会留在对象内,允许你在不同情境中重复使用它。因此,你需要的这些对象会更少,因为它们仅在内在状态上有所不同,而内在状态的变化远少于外在状态。

我们回到游戏吧。假设我们已经从粒子类中提取了外部态,游戏中只需三种不同的物体来表示所有粒子:子弹、导弹和一块弹片。正如你可能已经猜到的,只存储内在状态的对象叫做蝇量级.

外在状态存储

外在状态会转移到哪里?还是会有职业保存这些信息,对吧?在大多数情况下,它会被迁移到容器对象,容器对象在应用模式前会聚合对象。

在我们的情况下,它是存储场中所有粒子的主要对象。要将外在态移入该类,你需要创建多个数组字段,用于存储坐标、矢量和每个粒子的速度。但这还不是全部。你需要另一个数组来存储代表粒子的特定蝇量的引用。这些数组必须同步,这样你才能使用同一个索引访问粒子的所有数据。

更优雅的解决方案是创建一个独立的上下文类,存储外部状态以及对飞重对象的引用。这种方法需要容器类中只有一个数组。

等一下!我们是否需要像最初那样拥有同样多的这些情境物品?从技术上讲,是的。但问题是,这些天体比以前小得多。最耗费内存的领域被转移到了少数轻量级对象。现在,一千个小型上下文对象可以重复使用一个重磅对象,而不是存储一千份数据副本。

蝇量与不可变性

由于同一个蝇量级对象可以在不同上下文中使用,你必须确保它的状态不能被修改。一个飞量级只需通过构造参数初始化一次状态。它不应该让任何 setter 或公共场暴露给其他物体。

蝇量级工厂

为了更方便地获取各种蝇量级,你可以创建一个工厂方法来管理现有的蝇量级物品池。该方法接受客户端的目标蝇重的内在状态,寻找与该状态匹配的现有蝇重对象,若找到则返回该对象。如果没有,它会创建一个新的蝇量级并将其加入池中。

这种方法有多种可能。最明显的地方是轻量级容器。或者,你也可以创建一个新的工厂职业。或者你可以把工厂方法做成静态,放在真正的蝇量级里。

四.结构

五.适合应用场景

  1. 只有当程序必须支持大量几乎无法容纳于可用内存的对象时,才使用Flyweight模式。

    应用图案的好处很大程度上取决于它如何以及在哪里使用。它最有用的是:

    • 应用程序需要生成大量相似的对象

    • 这会消耗目标设备上所有可用的内存

    • 这些对象包含可提取并在多个对象之间共享的重复状态

六.实现方式

  1. 将将成为蝇量级的类别的场地分为两部分:

    • 内在状态:包含多个对象间重复不变数据的字段

    • 外在状态:包含每个对象独有的上下文数据的字段

  2. 保留代表本质状态的场,但确保它们是不可变的。它们应仅在构造子内部取初始值。

  3. 讲讲使用外在状态场的方法。对于方法中使用的每个字段,引入一个新参数并用它代替字段。

  4. 可选地创建工厂级别来管理蝇量级选手池。它应该在创建新蝇量之前检查已有的蝇量级。工厂建成后,客户只需通过该厂要求蝇量级。他们应通过将本有状态传递给工厂来描述所需的蝇量。

  5. 客户端必须存储或计算外部状态(上下文)的值,才能调用飞量对象的方法。为了方便起见,外在状态和蝇量引用字段可以被移到另一个上下文类。

七.优缺点

  1. 优点:

    • 假设你的程序有很多类似的对象,可以节省大量内存。
  2. 缺点

    • 你可能在用CPU周期交换内存,因为每次调用飞权方法时都需要重新计算上下文数据。

    • 代码变得更加复杂。新团队成员总会好奇为什么一个实体的状态会被这样分离。

八.与其他模式的关系

  • 你可以将复合树的共享叶节点作为蝇权重实现,以节省内存。

  • Flyweight展示了如何制作许多小物体,而Facade则展示了如何制作代表整个子系统的单一物体。

  • 如果你设法将所有共享状态的物体简化为一个蝇量级物体,蝇量级会类似于单例。但这些模式之间有两个根本区别:

    1. 单例体应当只有一个,而轻量级可以有多个具有不同内在状态的实例。

    2. 单例对象可以是可变的。蝇量级物体是不可变的。

九.示例代码

复制代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
​
// ====================== 1. 抽象享元类 ======================
class ChessFlyweight {
public:
    virtual ~ChessFlyweight() = default;
    // 棋子的业务方法:落子,接收【外蕴状态:坐标x,y】
    virtual void downChess(int x, int y) = 0;
    // 获取内蕴状态(棋子颜色)
    virtual string getColor() const = 0;
};
​
// ====================== 2. 具体享元类 ======================
class ConcreteChess : public ChessFlyweight {
private:
    string color; // 内蕴状态:棋子颜色(黑/白),不变、共享、创建后不修改
public:
    // 构造函数:初始化【内蕴状态】
    explicit ConcreteChess(string c) : color(std::move(c)) {}
​
    // 实现落子方法,内蕴状态(颜色)+外蕴状态(坐标)结合使用
    void downChess(int x, int y) override {
        cout << "在坐标(" << x << "," << y << ") 落下【" << color << "棋】" << endl;
    }
​
    string getColor() const override {
        return color;
    }
};
​
// ====================== 3. 享元工厂类 ======================
class ChessFactory {
private:
    // 对象池:缓存已创建的享元对象,key=内蕴状态(颜色),value=具体享元对象指针
    unordered_map<string, ChessFlyweight*> chessPool;
    // 单例模式(可选,推荐):保证全局只有一个享元工厂,统一管理对象池
    ChessFactory() = default;
public:
    // 全局唯一获取工厂的接口
    static ChessFactory& getInstance() {
        static ChessFactory factory;
        return factory;
    }
​
    // 核心方法:获取享元对象(复用核心逻辑)
    ChessFlyweight* getChess(const string& color) {
        // 1. 先查缓存:存在则直接返回复用
        if (chessPool.find(color) != chessPool.end()) {
            return chessPool[color];
        }
        // 2. 缓存不存在:创建新对象,存入缓存,再返回
        ChessFlyweight* chess = new ConcreteChess(color);
        chessPool[color] = chess;
        cout << ">>> 首次创建【" << color << "棋】对象 <<<" << endl;
        return chess;
    }
​
    // 获取对象池大小(测试用:验证内存中只有2个对象)
    int getPoolSize() const {
        return chessPool.size();
    }
​
    // 析构:释放对象池内存
    ~ChessFactory() {
        for (auto& pair : chessPool) {
            delete pair.second;
            pair.second = nullptr;
        }
        chessPool.clear();
    }
};
​
// ====================== 测试主函数 ======================
int main() {
    // 获取唯一的享元工厂
    ChessFactory& factory = ChessFactory::getInstance();
​
    // 落多个黑子:复用同一个黑子对象
    ChessFlyweight* black1 = factory.getChess("黑");
    black1->downChess(3, 3);
    ChessFlyweight* black2 = factory.getChess("黑");
    black2->downChess(5, 5);
    ChessFlyweight* black3 = factory.getChess("黑");
    black3->downChess(7, 7);
​
    cout << "---------------------------------" << endl;
​
    // 落多个白子:复用同一个白子对象
    ChessFlyweight* white1 = factory.getChess("白");
    white1->downChess(4, 4);
    ChessFlyweight* white2 = factory.getChess("白");
    white2->downChess(6, 6);
​
    cout << "---------------------------------" << endl;
    // 验证:内存中只创建了2个对象!!!
    cout << "享元对象池的大小:" << factory.getPoolSize() << endl;
​
    // 验证:所有黑子都是同一个对象,所有白子都是同一个对象
    cout << "black1 和 black2 是同一个对象:" << (black1 == black2) << endl;
    cout << "white1 和 white2 是同一个对象:" << (white1 == white2) << endl;
​
    return 0;
}

执行结果

复制代码
>>> 首次创建【黑棋】对象 <<<
在坐标(3,3) 落下【黑棋】
在坐标(5,5) 落下【黑棋】
在坐标(7,7) 落下【黑棋】
---------------------------------
>>> 首次创建【白棋】对象 <<<
在坐标(4,4) 落下【白棋】
在坐标(6,6) 落下【白棋】
---------------------------------
享元对象池的大小:2
black1 和 black2 是同一个对象:1
white1 和 white2 是同一个对象:1
相关推荐
小码过河.1 天前
设计模式——享元模式
java·设计模式·享元模式
亲爱的非洲野猪5 天前
深入解析享元模式:用Java实现高性能对象复用
java·开发语言·享元模式
Geoking.5 天前
【设计模式】享元模式(Flyweight)详解:用共享对象对抗内存爆炸
java·设计模式·享元模式
Engineer邓祥浩5 天前
设计模式学习(13) 23-11 享元模式
学习·设计模式·享元模式
sxlishaobin11 天前
设计模式之享元模式
java·设计模式·享元模式
阿闽ooo20 天前
深入浅出享元模式:从图形编辑器看对象复用的艺术
c++·设计模式·编辑器·享元模式
老朱佩琪!21 天前
Unity享元模式
unity·游戏引擎·享元模式
JavaBoy_XJ23 天前
结构型-享元模式
享元模式
.简.简.单.单.23 天前
Design Patterns In Modern C++ 中文版翻译 第十一章 享元模式
c++·设计模式·享元模式