14. 享元模式:模式动机与定义、角色与结构、C++代码实现、池化技术应用

14.1 模式动机与定义

先聊聊一个我踩过的坑。几年前我接手一个游戏引擎的优化任务,场景里要渲染上千棵树木。每棵树都有自己的位置、颜色、大小,但它们的网格模型和纹理是完全一样的。最初的设计是每棵树都独立持有完整的模型数据——结果内存直接爆了。

你想想看,一千棵树,每棵树都存一份几兆的网格数据,这合理吗?不合理。真正需要区分的,只是每棵树的位置和朝向这些“外部状态”。而网格和纹理这些“内部状态”,完全可以共享。

这就是享元模式要解决的问题。它的核心思想很简单:通过共享细粒度对象来减少内存消耗。说白了,就是把对象拆成两部分——共享的部分(内部状态)和变化的部分(外部状态)。共享的部分大家共用,变化的部分由调用方传入。

定义:享元模式(Flyweight Pattern)运用共享技术有效地支持大量细粒度对象的复用。它通过将对象的内部状态(可共享)与外部状态(不可共享)分离,减少内存中对象的数量。

14.2 角色与结构

享元模式涉及四个角色,我习惯把它们分成两组来理解:

  • 享元接口(Flyweight):声明一个接口,用于接收外部状态并执行操作。
  • 具体享元(ConcreteFlyweight):实现享元接口,存储内部状态。这部分是可以共享的。
  • 非共享具体享元(UnsharedConcreteFlyweight):有些对象不适合共享,但为了统一接口,也继承自享元接口。
  • 享元工厂(FlyweightFactory):创建并管理享元对象。它维护一个池子,当请求一个享元时,先检查池子里有没有,有就直接返回,没有就创建。

结构上其实不复杂,我画了一张图帮你理清关系:

FlyweightFactory - flyweights: map<string, Flyweight*> + getFlyweight(key): Flyweight* Flyweight(接口) + operation(extrinsicState) ConcreteFlyweight - intrinsicState: 内部状态 + operation(extrinsicState) UnsharedFlyweight - allState: 全部状态 + operation(extrinsicState) Client 持有外部状态

14.3 C++代码实现

直接上代码。还是用那个树木的例子,这样你更容易理解。

// 享元接口
class TreeModel {
public:
    virtual ~TreeModel() = default;
    virtual void render(float x, float y, float scale) const = 0;
};

// 具体享元——共享的树木模型
class SharedTreeModel : public TreeModel {
public:
    SharedTreeModel(const std::string& meshData, const std::string& textureData)
        : mesh_(meshData), texture_(textureData) {
        // 内部状态:网格和纹理,所有树共享
    }

    void render(float x, float y, float scale) const override {
        // 外部状态:位置和缩放,由调用方传入
        std::cout << "渲染树木: 网格=" << mesh_
                  << ", 纹理=" << texture_
                  << ", 位置=(" << x << ", " << y << ")"
                  << ", 缩放=" << scale << std::endl;
    }

private:
    std::string mesh_;     // 内部状态
    std::string texture_;  // 内部状态
};

// 享元工厂
class TreeModelFactory {
public:
    ~TreeModelFactory() {
        for (auto& pair : pool_) {
            delete pair.second;
        }
    }

    const TreeModel* getTreeModel(const std::string& type) {
        auto it = pool_.find(type);
        if (it != pool_.end()) {
            std::cout << "复用已有模型: " << type << std::endl;
            return it->second;
        }

        // 创建新模型
        std::string mesh = type + "_mesh";
        std::string texture = type + "_texture";
        SharedTreeModel* model = new SharedTreeModel(mesh, texture);
        pool_[type] = model;
        std::cout << "创建新模型: " << type << std::endl;
        return model;
    }

private:
    std::unordered_map<std::string, SharedTreeModel*> pool_;
};

// 客户端代码
class Tree {
public:
    Tree(const TreeModel* model, float x, float y, float scale)
        : model_(model), x_(x), y_(y), scale_(scale) {}

    void render() const {
        model_->render(x_, y_, scale_);
    }

private:
    const TreeModel* model_;  // 共享的享元对象
    float x_, y_, scale_;     // 外部状态,由客户端持有
};

// 使用示例
int main() {
    TreeModelFactory factory;

    // 创建1000棵橡树
    std::vector<Tree> forest;
    for (int i = 0; i < 1000; ++i) {
        const TreeModel* model = factory.getTreeModel("oak");
        forest.emplace_back(model, i * 1.0f, 0.0f, 1.0f);
    }

    // 创建1000棵松树
    for (int i = 0; i < 1000; ++i) {
        const TreeModel* model = factory.getTreeModel("pine");
        forest.emplace_back(model, 0.0f, i * 1.0f, 1.0f);
    }

    // 渲染所有树
    for (const auto& tree : forest) {
        tree.render();
    }

    return 0;
}
我的经验:注意看,工厂里只创建了两个模型对象("oak"和"pine"),但客户端可以生成2000棵树。这就是享元模式的威力——内存占用从2000份模型数据降到了2份。

14.4 池化技术应用

说到享元模式,就不得不提池化技术。其实享元工厂本身就是一个对象池。我在实际项目中,池化技术用得最多的是这三个场景:

池化类型 共享对象 外部状态 典型应用
对象池 数据库连接、线程 连接参数、任务数据 连接池、线程池
缓存池 计算结果、资源数据 查询参数、上下文 Redis缓存、内存缓存
实例池 游戏对象、UI控件 位置、状态、属性 子弹池、粒子系统

我曾经在一个网络库中实现过连接池,本质上就是享元模式。每个连接对象是共享的,但每次请求的地址和端口是外部状态。嗯,这里要注意:池化技术虽然省内存,但带来了线程安全问题。共享对象如果被多个线程同时修改,那就麻烦了。

避坑指南:我曾经在实现对象池时,忘记考虑线程安全,结果在高并发下出现了连接被重复使用的问题。后来加上了互斥锁和引用计数才解决。记住:享元对象必须是不可变的,或者做好同步控制。

再补充一个实际案例。我在做UI框架时,每个按钮、文本框都是一个控件对象。如果每个窗口都创建独立的控件实例,内存开销很大。后来我把控件的样式(字体、颜色、边框)作为内部状态共享,只把文本内容和位置作为外部状态。这样,一个对话框里几百个控件,实际上只用了十几种样式对象。

总结一下享元模式的使用要点:

  • 适用场景:系统中存在大量相似对象,且这些对象可以拆分为内部状态和外部状态
  • 内部状态:存储在享元对象中,不随环境改变,可以共享
  • 外部状态:由客户端维护,每次调用时传入,不可共享
  • 工厂管理:通过工厂创建和管理享元对象,避免重复创建

说白了,享元模式就是“抠门”的艺术——能省就省,能共享绝不重复创建。但也要注意,不是所有场景都适合用。如果对象数量不多,或者内部状态和外部状态耦合太紧,强行用享元反而会让代码变得复杂。这个度,需要你在实际项目中慢慢体会。

公众号:蓝海资料掘金营,微信 deep3321