Chapter 11 — 享元模式 Flyweight
灵魂速记:共享单车——大家骑同一批车,不用每人买一辆。
秒懂类比
一个城市有 100 万人要骑自行车,你不需要造 100 万辆。造 5 万辆共享单车,谁用谁骑,用完还回去。
享元模式的核心:把大量对象中相同的部分提取出来共享,减少内存占用。
问题引入
// 灾难现场:游戏中 10000 棵树classTree{doublex_,y_;// 位置(每棵不同)std::string name_;// "Oak"(很多棵一样)std::string color_;// "green"(很多棵一样)Texture texture_;// 1MB 大小的纹理!(很多棵一样)};// 10000 棵树 × 1MB 纹理 = 10GB 内存// 💥 内存爆炸!观察:10000 棵树中,可能只有 3 种类型(橡树、松树、桦树)。每种树的纹理是一样的,没必要存 10000 份!
内在状态 vs 外在状态
| 内在状态(Intrinsic) | 外在状态(Extrinsic) | |
|---|---|---|
| 定义 | 可以共享的、不变的部分 | 每个对象独有的、变化的部分 |
| 例子 | 树的纹理、颜色、名称 | 树的位置 x, y |
| 存储 | 享元对象中(共享) | 外部传入(不共享) |
模式结构
┌────────────────┐ │ FlyweightFactory│ ← 管理共享对象的池子 ├────────────────┤ │ +getFlyweight()│ ← 有就复用,没有才创建 └───────┬────────┘ │ 返回 ┌───────┴────────┐ │ Flyweight │ ← 共享的对象(内在状态) ├────────────────┤ │ -intrinsicState│ │ +operation( │ │ extrinsicState)│ ← 外在状态从外部传入 └────────────────┘C++ 实现
#include<iostream>#include<memory>#include<string>#include<unordered_map>#include<vector>// ========== 享元:树的类型(共享部分) ==========classTreeType{public:TreeType(std::string name,std::string color,std::string textureData):name_(std::move(name)),color_(std::move(color)),textureData_(std::move(textureData)){std::cout<<" [创建纹理] "<<name_<<" ("<<textureData_.size()<<" bytes)\n";}voiddraw(doublex,doubley)const{std::cout<<" 画 "<<name_<<"("<<color_<<") at ("<<x<<", "<<y<<")\n";}private:std::string name_;// 内在状态std::string color_;// 内在状态std::string textureData_;// 内在状态(大对象!)};// ========== 享元工厂 ==========classTreeTypeFactory{public:staticstd::shared_ptr<TreeType>getTreeType(conststd::string&name,conststd::string&color){std::string key=name+"_"+color;autoit=cache_.find(key);if(it!=cache_.end()){returnit->second;// 已有,直接复用}// 没有,创建新的(模拟大纹理数据)autotype=std::make_shared<TreeType>(name,color,std::string(1024*1024,'#'));// 模拟 1MBcache_[key]=type;returntype;}staticsize_ttypeCount(){returncache_.size();}private:// static inline:C++17 特性,允许在类内直接初始化静态成员变量。// 没有 inline 的话,你得在 .cpp 文件里写 std::unordered_map<...> TreeTypeFactory::cache_;// 有了 inline,声明和定义合一,和 Ch01 讲的 inline 变量是同一个道理。staticinlinestd::unordered_map<std::string,std::shared_ptr<TreeType>>cache_;};// ========== 具体的树(外在状态 + 享元引用) ==========classTree{public:Tree(doublex,doubley,std::shared_ptr<TreeType>type):x_(x),y_(y),type_(std::move(type)){}voiddraw()const{type_->draw(x_,y_);// 把外在状态传给享元}private:doublex_,y_;// 外在状态(每棵树不同)std::shared_ptr<TreeType>type_;// 指向共享的享元};// ========== 森林 ==========classForest{public:voidplantTree(doublex,doubley,conststd::string&name,conststd::string&color){autotype=TreeTypeFactory::getTreeType(name,color);trees_.emplace_back(x,y,type);}voiddraw()const{for(constauto&tree:trees_){tree.draw();}}size_ttreeCount()const{returntrees_.size();}private:std::vector<Tree>trees_;};intmain(){Forest forest;std::cout<<"=== 种树 ===\n";// 种 1000 棵树,但只有 3 种类型for(inti=0;i<500;++i){forest.plantTree(i*1.0,i*0.5,"Oak","green");}for(inti=0;i<300;++i){forest.plantTree(i*2.0,i*1.0,"Pine","dark_green");}for(inti=0;i<200;++i){forest.plantTree(i*0.5,i*3.0,"Birch","white");}std::cout<<"\n=== 统计 ===\n";std::cout<<"树的总数: "<<forest.treeCount()<<"\n";std::cout<<"树类型数: "<<TreeTypeFactory::typeCount()<<"\n";std::cout<<"无享元内存: "<<forest.treeCount()<<" MB (每棵 1MB 纹理)\n";std::cout<<"有享元内存: "<<TreeTypeFactory::typeCount()<<" MB (只有 "<<TreeTypeFactory::typeCount()<<" 份纹理)\n";// 节省了 997MB!}输出:
=== 种树 === [创建纹理] Oak (1048576 bytes) [创建纹理] Pine (1048576 bytes) [创建纹理] Birch (1048576 bytes) === 统计 === 树的总数: 1000 树类型数: 3 无享元内存: 1000 MB (每棵 1MB 纹理) 有享元内存: 3 MB (只有 3 份纹理)1000 棵树,只创建了 3 个纹理对象。内存节省 99.7%。
什么时候用?
| ✅ 适合 | ❌ 别用 |
|---|---|
| 有大量相似对象 | 对象数量少 |
| 对象的大部分状态可以外部化 | 每个对象都完全不同 |
| 内存是瓶颈 | 内存充裕,不需要优化 |
| 对象状态可分为内在/外在 | 无法拆分内在/外在状态 |
经验法则:对象数量在千级以上,且有大量重复数据时再考虑。
防混淆
Flyweight vs Singleton
| Flyweight | Singleton | |
|---|---|---|
| 实例数量 | 多个(但共享重复的) | 只有一个 |
| 目的 | 节省内存 | 保证唯一 |
| 状态 | 内在状态不可变 | 可以有可变状态 |
一句话分清:Singleton 保证只有 1 个,Flyweight 保证共享不重复。
Flyweight vs 对象池
| Flyweight | 对象池 | |
|---|---|---|
| 共享方式 | 同时共享(多个持有者) | 借出归还(一次一个使用) |
| 可变性 | 内在状态不可变 | 对象可变 |
| 类比 | 共享单车(多人同时看到车) | 图书馆(借走了别人就不能借) |
现实中的 Flyweight
| 场景 | 享元 | 共享了什么 |
|---|---|---|
| 字符串池 | std::string/ Java String Pool | 相同的字符串值 |
| 字体渲染 | 字符字形 | 同一字体同一字号的字形 |
| 游戏粒子系统 | 粒子类型 | 纹理、物理属性 |
| 围棋/象棋程序 | 棋子类型 | “黑子”"白子"的外观 |