代码改变世界

精读C++20设计模式——结构型设计模式:享元模式 - 实践

2025-10-25 15:07  tlnshuju  阅读(6)  评论(0)    收藏  举报

精读C++20设计模式——结构型设计模式:享元模式

前言

​ 现在我们来仔细学习一下享元模式:Flyweight,但是我觉得好像叫 Token 或者是 Cookie更加的合适(原书并列的说,笔者认为后两个说辞我显然更加能接受和理解),它主要是尝试解决一种性能问题——我们可不可以复用一些已经有的东西呢(注意我们没有在复用抽象,而是在复用数据,更加像是期待用引用取代值)

一个例子:文本显示

​ 假设我们现在正在打开一本超级无敌大的书,大约几千万个字吧!但是我们知道,常用的汉字本身可能也就不到 4000 个。比如说,在整本书中,大量出现了“我”、“你”、“吧”等高频词汇。那问题来了:我们真的有必要在内存里反复存储一大堆一模一样的字形数据吗?显然没有必要。

​ 我们完全可以不存储字本身的完整信息,而是存储指向这个字的索引。换句话说,就是把“字”放到一个共享的字典池里,文本本身只记录“引用”。这样,当需要显示时,再根据索引去取对应的字形。这种方式就避免了冗余存储,从而极大节省了内存空间。

那为什么 ASCII 文本中我们没有这么做呢?原因在于代价问题。ASCII 编码本身只占一个字节,而指针或索引往往需要 2~4 个字节(甚至更多),这样一来就得不偿失了。换句话说,ASCII 字符已经足够小,继续压缩反而增加了额外负担。

​ 不过,事情并没有到此为止。让我们继续思考:如果不仅仅是单个字频繁出现,还有一些字的组合或搭配也非常常见,比如“你好”、“谢谢”、“没事吧”。在整本书的文本里,这些词组也会反复出现。那我们是不是也可以用同样的方法,把它们放到共享池里,然后文本只存储索引?

​ 这样一来,我们就不仅减少了对单字的重复存储,还进一步减少了对常见词组的重复存储。随着共享池的规模合理扩展,我们就能用有限的资源,存储和渲染出极其庞大的文本。这种思想,其实就是**享元模式(Flyweight Pattern)**在文本显示场景下的一个具体应用。

#include <iostream>
  #include <unordered_map>
    #include <memory>
      #include <string>
        #include <vector>
          // 享元对象:表示一个“字”或“词组”
          class Glyph {
          public:
          explicit Glyph(const std::string& content) : content_(content) {}
          void draw() const {
          std::cout << content_;
          }
          private:
          std::string content_;
          };
          // 享元工厂:管理所有的共享对象
          class GlyphFactory {
          public:
          std::shared_ptr<Glyph> getGlyph(const std::string& content) {
            auto it = pool_.find(content);
            if (it != pool_.end()) {
            return it->second; // 已存在,直接复用
            }
            auto glyph = std::make_shared<Glyph>(content);
              pool_[content] = glyph;
              return glyph;
              }
              private:
              std::unordered_map<std::string, std::shared_ptr<Glyph>> pool_;
                };
                // 客户端:文本渲染
                class Document {
                public:
                void addWord(const std::shared_ptr<Glyph>& glyph) {
                  text_.push_back(glyph);
                  }
                  void render() const {
                  for (auto& g : text_) {
                  g->draw();
                  }
                  std::cout << std::endl;
                  }
                  private:
                  std::vector<std::shared_ptr<Glyph>> text_;
                    };
                    int main() {
                    GlyphFactory factory;
                    Document doc;
                    // 假设整本书有很多 "你", "好", "吧"
                    auto g1 = factory.getGlyph("你");
                    auto g2 = factory.getGlyph("好");
                    auto g3 = factory.getGlyph("吧");
                    // 多次重复使用,不会创建新的对象
                    doc.addWord(g1);
                    doc.addWord(g2);
                    doc.addWord(g3);
                    doc.addWord(g1);
                    doc.addWord(g2);
                    doc.render(); // 输出:你好吧你好
                    }
棋盘游戏
#include <iostream>
  #include <unordered_map>
    #include <memory>
      // 享元对象:棋子
      class ChessPiece {
      public:
      ChessPiece(const std::string& color, const std::string& type)
      : color_(color), type_(type) {}
      void draw(int x, int y) const {
      std::cout << color_ << type_ << " 放在 (" << x << "," << y << ")\n";
      }
      private:
      std::string color_; // 内部状态:颜色
      std::string type_;  // 内部状态:棋子类型
      };
      // 工厂:保证相同的棋子只创建一次
      class ChessFactory {
      public:
      std::shared_ptr<ChessPiece> getChess(const std::string& color, const std::string& type) {
        std::string key = color + type;
        if (pool_.count(key)) return pool_[key];
        auto piece = std::make_shared<ChessPiece>(color, type);
          pool_[key] = piece;
          return piece;
          }
          private:
          std::unordered_map<std::string, std::shared_ptr<ChessPiece>> pool_;
            };
            int main() {
            ChessFactory factory;
            auto blackPawn = factory.getChess("黑", "卒");
            auto redPawn   = factory.getChess("红", "兵");
            // 外部状态:位置
            blackPawn->draw(2, 3);
            redPawn->draw(5, 6);
            blackPawn->draw(2, 4);
            }

​ 我们可以得到输出

黑卒 放在 (2,3)
红兵 放在 (5,6)
黑卒 放在 (2,4)

即使棋盘上有 16 个卒,它们在内存中只会存在两个共享对象(黑卒、红兵)。


数据库连接池
#include <iostream>
  #include <unordered_map>
    #include <memory>
      // 享元对象:连接配置
      class DBConfig {
      public:
      DBConfig(const std::string& host, int port) : host_(host), port_(port) {}
      void show() const {
      std::cout << "DBConfig: " << host_ << ":" << port_ << "\n";
      }
      private:
      std::string host_;
      int port_;
      };
      // 工厂:共享相同配置
      class DBConfigFactory {
      public:
      std::shared_ptr<DBConfig> getConfig(const std::string& host, int port) {
        std::string key = host + ":" + std::to_string(port);
        if (pool_.count(key)) return pool_[key];
        auto cfg = std::make_shared<DBConfig>(host, port);
          pool_[key] = cfg;
          return cfg;
          }
          private:
          std::unordered_map<std::string, std::shared_ptr<DBConfig>> pool_;
            };
            int main() {
            DBConfigFactory factory;
            auto c1 = factory.getConfig("127.0.0.1", 3306);
            auto c2 = factory.getConfig("127.0.0.1", 3306);
            auto c3 = factory.getConfig("192.168.1.10", 5432);
            c1->show();
            c2->show();
            c3->show();
            }
咋用啊?
  • 打印店字体库:一台电脑装了一份宋体字库,所有人都用这份库。没有必要为每份文档都单独保存字形。
  • 地铁 IC 卡:内部状态是固定的芯片结构,外部状态是余额、有效期等随用户变化的数据。
  • 视频游戏贴图:相同的树木、草丛、石头模型在地图里出现成百上千次,但只加载一份纹理资源,实例化时传入不同的位置和缩放参数。

总结

我们要解决什么问题?

当系统中存在大量相似对象时,内存占用可能急剧膨胀,性能下降。
但这些对象其实往往存在可复用的内部状态(如字形、纹理、连接配置),如果能共享这部分,就能显著节省资源。

享元模式要解决的核心问题就是:如何在保证功能的前提下,尽量减少内存中的冗余对象。


我们如何解决?

享元模式通过将对象的状态拆分为两类:

  1. 内部状态(Intrinsic State)
    • 对象本身不会随环境变化的、不变的、可被共享的部分。
    • 例如:汉字的字形、棋子的颜色与类型、数据库连接参数。
  2. 外部状态(Extrinsic State)
    • 与上下文相关的、每次使用时才决定的部分。
    • 例如:汉字在文档中的位置、棋子在棋盘上的坐标、连接池中的使用次数。

通过共享内部状态对象 + 外部状态由调用方提供,就能极大减少对象数量。

享元模式优缺点
优点缺点
显著减少内存占用系统复杂度增加,需要区分内外部状态
提升对象复用率不适合对象完全不同、共享价值不大的场景
便于维护共享资源池外部状态需要额外传递,增加调用方负担