2023 跟我一起学设计模式: 享元模式 (Cache)

享元形式规划形式

享元形式是一种结构型规划形式, 它不运用在每个方针中保存一切数据的办法, 而是经过同享多个方针所共有的相同状况, 让开发者能在有限的内存容量中载入更多方针的一种规划形式。

遇到的问题

假设你期望在长期作业后放松一下, 所以开发了一款简略的游戏: 玩家们在地图上移动并相互射击。 你决议完成一个实在的粒子体系, 并将其作为游戏的特征。 许多的子弹、 导弹和爆破弹片会在整个地图上穿行, 为玩家供给紧张影响的游戏体会。

开发完成后, 你推送提交了最新版别的程序, 并在编译游戏后将其发送给了一个朋友进行测验。 尽管该游戏在你的电脑上完美运转, 可是你的朋友却无法长期进行游戏: 游戏总是会在他的电脑上运转几分钟后溃散。 在你查找了好几个小时的 debug信息记录后, 你发现导致游戏溃散的原因是机器内存容量不足。 由于朋友的设备功能远比不上你的电脑, 所以游戏运转在他的电脑上时很快就会出现内存耗尽的问题。

真实的问题与粒子体系有关。 每个粒子 (一颗子弹、 一枚导弹或一块弹片) 都由包括完整数据的独立方针来表明。 当玩家在游戏中激战进入高潮后的某一时刻, 游戏将无法在剩下内存中载入新建粒子, 所以程序就溃散了。

2023 跟我一起学设计模式: 享元模式 (Cache)

怎样处理呢?

仔细观察 粒子Particle类, 你可能会注意到颜色 (color) 和精灵图 (sprite)这两个成员变量所耗费的内存要比其他变量多得多。 更糟糕的是, 对于一切的粒子来说, 这两个成员变量所存储的数据几乎彻底相同 (比如一切子弹的颜色和精灵图都相同)。

2023 跟我一起学设计模式: 享元模式 (Cache)

每个粒子的另一些状况 (坐标、 移动矢量和速度) 则是不同的。 由于这些成员变量的数值会不断改动。 这些数据代表粒子在存续期间不断改动的情形, 但每个粒子的颜色和精灵图则会坚持不变。

方针的常量数据一般被称为内涵状况, 其位于方针中, 其他方针只能读取但不能修改其数值。 而方针的其他状况常常能被其他方针 “从外部” 改动, 因而被称为外在状况

享元形式建议不在方针中存储外在状况, 而是将其传递给依赖于它的一个特殊办法。 程序只在方针中保存内涵状况, 以方便在不同情形下重用。 这些方针的差异仅在于其内涵状况 (与外在状况比较, 内涵状况的变体要少许多), 因而你所需的方针数量会大大削减。

2023 跟我一起学设计模式: 享元模式 (Cache)

让咱们回到游戏中。 假设能从粒子类中抽出外在状况, 那么咱们只需三个不同的方针 (子弹、 导弹和弹片) 就能表明游戏中的一切粒子。 你现在很可能已经猜到了, 咱们将这样一个仅存储内涵状况的方针称为享元。

外在状况存储

那么外在状况会被移动到什么地方呢? 总得有类来存储它们, 对不对? 在大部分状况中, 它们会被移动到容器方针中, 也便是咱们运用享元形式前的聚合方针中。

在咱们的例子中, 容器方针便是首要的 游戏方针, 其会将一切粒子存储在名为 粒子particles的成员变量中。 为了能将外在状况移动到这个类中, 你需求创立多个数组成员变量来存储每个粒子的坐标、 方向矢量和速度。 除此之外, 你还需求另一个数组来存储指向代表粒子的特定享元的引证。 这些数组有必要坚持同步, 这样你才干够运用同一索引来获取关于某个粒子的一切数据。

2023 跟我一起学设计模式: 享元模式 (Cache)

更好的处理方案是创立独立的情形类来存储外在状况和对享元方针的引证。 在该办法中, 容器类只需包括一个数组。

假如这样的话情形方针数量会不会和不采用该形式时的方针数量相同多吗? 是相同多,但这些方针要比之前小许多。 耗费内存最多的成员变量已经被移动到很少的几个享元方针中了。 至此,一个享元大方针会被上千个情境小方针复用, 所以无需再重复存储数千个大方针的数据。

享元工厂

为了能更方便地访问各种享元, 开发者能够创立一个工厂办法来办理已有享元方针的缓存池。 工厂办法从客户端处接收方针享元方针的内涵状况作为参数, 假如它能在缓存池中找到所需享元, 则将其回来给客户端; 假如没有找到, 它就会新建一个享元, 并将其添加到缓存池中。

你能够挑选在程序的不同地方放入该函数。 最简略的挑选便是将其放置在享元容器中。 除此之外, 你还能够新建一个工厂类, 或者创立一个静态的工厂办法并将其放入实践的享元类中。

享元形式结构

  1. 享元形式仅仅一种优化。 在运用该形式之前, 你要确定程序中存在与许多类似方针一起占用内存相关的内存耗费问题, 并且保证该问题无法运用其他更好的办法来处理。
  2. 享元 (Flyweight) 类包括原始方针中部分能在多个方针中同享的状况。 同一享元方针可在许多不同情形中运用。 享元中存储的状况被称为 “内涵状况”。 传递给享元办法的状况被称为 “外在状况”。
  3. 情形 (Context) 类包括原始方针中各不相同的外在状况。 情形与享元方针组合在一起就能表明原始方针的全部状况。
  4. 一般状况下, 原始方针的行为会保存在享元类中。 因而调用享元办法有必要供给部分外在状况作为参数。 但开发者也可将行为移动到情形类中, 然后将连入的享元作为单纯的数据方针。
  5. 客户端 (Client) 负责核算或存储享元的外在状况。 在客户端看来, 享元是一种可在运转时进行配置的模板方针, 详细的配置办法为向其办法中传入一些情形数据参数。
  6. 享元工厂 (Flyweight Factory) 会对已有享元的缓存池进行办理。 有了工厂后, 客户端就无需直接创立享元, 它们只需调用工厂并向其传递方针享元的一些内涵状况即可。 工厂会依据参数在之前已创立的享元中进行查找, 假如找到满意条件的享元就将其回来; 假如没有找到就依据参数新建享元。

享元形式合适运用的场景

仅在程序有必要支撑许多方针且没有满足的内存容量时运用享元形式。

运用该形式所获的收益巨细取决于运用它的办法和情形。以下状况中最有用:

  • 程序需求生成数量巨大的类似方针
  • 这将耗尽方针设备的一切内存
  • 方针中包括可抽取且能在多个方针间同享的重复状况。

完成办法

  1. 将需求改写为享元的类成员变量拆分为两个部分:
    • 内涵状况: 包括不变的、 可在许多方针中重复运用的数据成员变量。
    • 外在状况: 包括每个方针各自不同的情形数据的成员变量
  2. 保存类中表明内涵状况的成员变量, 并将其特点设置为不可修改。 这些变量仅可在构造函数中获得初始数值。
  3. 找到一切运用外在状况成员变量的办法, 为在办法中所用的每个成员变量新建一个参数, 并运用该参数代替成员变量。
  4. 你能够有挑选地创立工厂类来办理享元缓存池, 它负责在新建享元时查看已有的享元。 假如挑选运用工厂, 客户端就只能经过工厂来请求享元, 它们需求将享元的内涵状况作为参数传递给工厂。
  5. 客户端有必要存储和核算外在状况 (情形) 的数值, 由于只有这样才干调用享元方针的办法。 为了运用方便, 外在状况和引证享元的成员变量能够移动到独自的情形类中。

享元形式长处和缺陷

  • 假如程序中有许多类似方针类, 那么将能够节省许多内存。
  • 需求献身执行速度来换取内存,也便是时刻换空间, 由于他人每次调用享元办法时都需求重新核算部分情形数据。
  • 代码会变得愈加复杂。

代码示例

  • Go 享元形式讲解和代码示例 – 掘金 ()