ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

结构型模式—享元(flyweight)模式

结构型模式—享元(flyweight)模式

目录

  • 定义
  • 核心思想
    • 示例代码(只有共享享元)
    • 示例代码(共享+非共享)
  • 实现
    • 类图
  • 优缺点
    • 优点
    • 缺点
  • 适用场景
  • 结构型六兄弟判据

定义

运用共享技术有效支持大量细粒度的对象

核心思想

把状态劈成两半。

享元模式的全部精髓,都浓缩在这一句话里:一个对象身上携带的数据,并不是生来平等的——有的数据随每个实例千变万化,有的数据在成千上万个实例之间完全一致。前者必须每个对象各存一份,后者却只需要存一份、大家共用。享元要做的,就是识别出这条分界线,把"共享的那一半"抽出来集中存放,把"变化的那一半"留在外面。

要讲清楚这条分界线画在哪里,得先把"状态"这个词本身拆开。OOP 标准公式:对象 = 身份 + 状态 + 行为。

  1. 身份:对象的身份就是它的存储地址——&a、this、指针比较 p1 == p2 比的都是身份;
  • 别名:引用/指针是"同一个身份的另一个名字",不是新对象;
  • C++ 的经典设计决策:class 类型默认没有——比身份(地址)还是比值(状态),语言不替你决定,是 operator的设计选择(Python 把这个分歧摆得最明:is 比身份、== 比值)。

身份与享元的关系值得单独强调:享元模式恰恰是主动放弃身份的一种设计。当一万棵树共享同一个TreeType时,这一万个Tree记录在"树型"这个维度上已经不再拥有各自独立的身份——它们指向同一个对象,&forest[i].type == &forest[j].type恒成立。享元用"共享身份"换来了内存,代价是这些对象在共享维度上不可再被单独区分、单独修改。理解这一点,才能理解后面"享元必须不可变"这条铁律的由来:一旦共享的对象可以被某个使用者改写,所有引用它的对象都会跟着"变样",身份共享立刻退化成全局污染。

  1. 状态(state): “它现在什么样”,对象在某一时刻携带的全部数据值——非静态数据成员的当前值(含嵌套成员递归展开)。关键性质:状态是快照、可变,而上一轮说过,"是否随上下文变化"正是享元劈状态的判据。

把这条判据落到操作层面,可以问三个问题来给每个字段归类:

  • 它会不会随使用场景变化?树的位置每棵都不同,会变;树的纹理贴图对所有松树都一样,不变。
  • 它能不能被多个对象同时使用而不冲突?位置被两棵树共用就乱套了;纹理被一万棵树共用毫无问题。
  • 它修改的频率高不高?高频修改的字段不适合共享,因为共享意味着"一改全改"。

三个问题都指向"不变、可共用、低频"的字段,就是 intrinsic 的候选;反之则是 extrinsic。注意这里的"不变"不是指永远不变,而是指在对象的整个生命周期内不需要变——享元对象一旦创建就冻结,这是共享能够成立的前提条件,而不是可有可无的优化。

  1. 行为(behavior)——“它能做什么”,对象可响应的操作——成员函数。注意归属:行为是类给的、全体实例共享一份代码;对象体内并不存放行为,只放一个 vptr 指向行为表(《深度探索C++对象模型》的经典结论:虚函数的对象布局 = vptr + 数据成员)。虚函数让"同一调用落到不同行为"——行为随动态类型选择,这正是多态。

行为这一维给享元提供了一个重要启示:代码本来就是共享的,享元共享的是数据。既然一万个对象调用同一个成员函数时,执行的始终是同一份机器指令,那么"共享"在行为层面早已是默认事实,享元只是把这种共享从代码层延伸到了数据层。这也解释了为什么享元的接口设计要格外小心:render(x, y)把 extrinsic 作为参数传入,本质上是让"共享的行为"去操作"各自的数据",行为共享与数据分离在这里达成了统一。
例子:一万棵树渲染到地图上,朴素的做法是建一万个树对象——但每个对象里真正“各不相同”的只有位置,树名/颜色/纹理对这些树都是一样的。

状态名字特征放哪
内部状态intrinsic树型(名/色/纹理)不变、可共享享元对象里,只读
外部状态extrinsic位置 (x, y)随实例变化、不可共享客户端持有,调用时传参

示例代码(只有共享享元)

classTreeType{// 享元:只有 intrinsicpublic:voidrender(doublex,doubley)const;// extrinsic 作为参数进门private:std::string name_;// 全部只读——不可变是共享的前提std::string color_;inttexture_;};structTree{// 客户端的轻量记录constTreeType&type;// 观察引用,不拥有doublex,y;// extrinsic 归客户};classTreeFactory{public:staticconstTreeType&get(std::string name,std::string color,inttexture){for(constauto&t:pool_){if(t->name()==name){return*t;// SHARE the existing flyweight}}pool_.push_back(std::make_unique<TreeType>(std::move(name),std::move(color),texture));return*pool_.back();}staticsize_tkinds(){returnpool_.size();}private:inlinestaticstd::vector<std::unique_ptr<TreeType>>pool_;// OWNING};intmain(){std::vector<Tree>forest;constchar*names[]={"pine","oak","birch"};constchar*colors[]={"green","dark","pale"};for(inti=0;i<1000;++i){constintk=i%3;forest.push_back({TreeFactory::get(names[k],colors[k],k+1),1.0+i*0.5,2.0+k});}std::puts("== render the first three ==");for(inti=0;i<3;++i){forest[i].type.render(forest[i].x,forest[i].y);}}

万级对象缩成“少量共享享元+万条轻量记录”

示例代码(共享+非共享)

classGlyph{// Flyweight 抽象public:virtualvoiddraw(intx,inty)const=0;// extrinsic 作参数virtuallongwidth()const=0;};classCharacter:publicGlyph{// ConcreteFlyweight:共享private:charch_;// intrinsic,构造后不可变};classRow:publicGlyph{// UnsharedConcreteFlyweight:不共享public:voidadd(constGlyph&g){children_.push_back(&g);}private:std::vector<constGlyph*>children_;// 观察引用——工厂拥有共享者};classGlyphFactory{// FlyweightFactorypublic:staticconstCharacter&get(charch){if(!pool_[ch-'a'])pool_[ch-'a']=std::make_unique<Character>(ch);return*pool_[ch-'a'];// 每个字符至多一份,重复请求共享}private:inlinestaticstd::array<std::unique_ptr<Character>,26>pool_;};

实现

类图

角色本例对应职责
抽象享元 FlyweightTreeType声明带 extrinsic 参数的接口
具体享元 ConcreteFlyweightTreeType(合一)存储 intrinsic,构造后不可变
享元工厂 FlyweightFactoryTreeFactory按键查池:有则共享、无则新建
客户 ClientTree, forest持 extrinsic + 享元的观察引用

示例代码和flyweight的类图关系

类关系类型C++ 落地
TreeType→ 抽象享元is-a公有继承(本例合一省略)
TreeFactory→ 享元池组合(独占)vector<unique_ptr<TreeType>>
Client →TreeType观察(引用)const TreeType&,不参与生命周期
Client → extrinsic自持记录结构体的普通成员

优缺点

优点

  • 内存数量级下降
  • 只读共享天然线程安全

缺点

  • extrinsic化的代价:本可以作为成员的状态要变成每次调用的外参,接口繁琐,客户端背状态
  • 工厂查对象有开销
  • 享元失效/淘汰 策略复杂
  • 为几千个对象上享元是过度设计

适用场景

  • 大量的细粒度对象和大比例可以提取的intrinsic,如:
    • 游戏场景,可视化地图
    • rviz中同类型的marker共享显示属性

结构型六兄弟判据

模式① 接口② 谁创建/持有③ 目的
Adapter变客户/装配接口转换
Decorator同客户装配加功能
Composite同容器持有(0…n)部分整体
Bridge两套注入(实现侧)分离维度
Proxy同代理自己控制访问
Flyweight同工厂独占池+客户观察共享不可变状态省内存
返回列表