圣诞大减价开始了!

轻量级选手

也被称为:缓存

意图

轻量级选手是一种结构设计模式,通过在多个对象之间共享公共状态部分,而不是将所有数据保存在每个对象中,可以将更多的对象放入可用的RAM中。

Flyweight设计模式

问题

为了在长时间工作后获得乐趣,你决定创造一款简单的电子游戏:玩家在地图上移动并互相射击。你选择执行一个逼真的粒子系统,并将其作为游戏的特色。大量的子弹、导弹和爆炸的弹片应该在地图上到处飞,给玩家带来令人兴奋的体验。

在游戏完成后,你完成了最后一次提交,创造了游戏并将其发送给好友进行测试。虽然游戏在你的机器上运行得很完美,但你的朋友却不能玩太久。在他的电脑上,游戏在玩了几分钟后一直死机。在花了几个小时挖掘调试日志后,你发现游戏崩溃是因为RAM不足。事实证明,你朋友的设备远不如你自己的电脑强大,这就是为什么问题很快就出现在他的机器上。

实际的问题与你的粒子系统有关。每个粒子,比如一颗子弹、一枚导弹或一块弹片,都由一个包含大量数据的独立物体表示。在某个时刻,当玩家屏幕上的屠杀达到高潮时,新生成的粒子不再适合剩余的RAM,因此程序崩溃。

Flyweight模式问题

解决方案

仔细检查粒子类,你可能会注意到颜色和雪碧字段比其他字段消耗更多的内存。更糟糕的是,这两个场在所有粒子中存储几乎相同的数据。例如,所有子弹都有相同的颜色和精灵。

Flyweight图案解决方案

粒子状态的其他部分,如坐标、运动矢量和速度,对于每个粒子都是唯一的。毕竟,这些字段的值会随着时间而变化。这些数据表示粒子所处的环境不断变化,而每个粒子的颜色和精灵保持不变。

对象的这个常量数据通常称为内在的状态。它存在于对象中;其他对象只能读取它,不能更改它。对象状态的其余部分,通常由其他对象“从外部”改变,称为外在状态

Flyweight模式建议停止在对象内部存储外部状态。相反,您应该将此状态传递给依赖它的特定方法。只有内在状态保留在对象中,允许您在不同的上下文中重用它。因此,你需要更少的这些对象,因为它们只是在内在状态上有所不同,而内在状态的变化要比外在状态少得多。

Flyweight图案解决方案

让我们回到我们的游戏。假设我们已经从粒子类中提取了外部状态,那么只有三个不同的物体就足以代表游戏中的所有粒子:一颗子弹、一枚导弹和一块弹片。正如您现在可能已经猜到的,只存储内在状态的对象被调用一个轻量级

外部状态存储

外在状态会向哪里移动?某些类仍然应该存储它,对吧?在大多数情况下,它被移动到容器对象,容器对象在我们应用模式之前聚合对象。

在我们的例子中,这是主要的游戏对象中存储所有粒子的粒子字段。为了将外部状态移动到这个类中,您需要创建几个数组字段来存储每个粒子的坐标、向量和速度。但这还不是全部。您需要另一个数组来存储对表示粒子的特定flyweight的引用。这些数组必须是同步的,这样你才能使用相同的索引访问一个粒子的所有数据。

Flyweight图案解决方案

一个更优雅的解决方案是创建一个单独的上下文类,该类将存储外部状态以及对flyweight对象的引用。这种方法只需要在容器类中有一个数组。

等一下!我们不需要像一开始一样多的上下文对象吗?从技术上来说,是的。但问题是,这些物体比以前小了很多。最消耗内存的字段已经被移动到几个轻量级对象。现在,一千个小型上下文对象可以重用单个重量级对象,而不是存储一千个数据副本。

轻量级和不变性

因为同一个flyweight对象可以在不同的上下文中使用,所以必须确保它的状态不能被修改。flyweight应该只通过构造函数参数初始化它的状态一次。它不应该向其他对象暴露任何setter或公共字段。

轻量级的工厂

为了更方便地访问各种flyweight,可以创建一个工厂方法来管理现有flyweight对象池。该方法从客户端接受所需flyweight的固有状态,查找与此状态匹配的现有flyweight对象,如果找到则返回该对象。如果没有,则创建一个新的flyweight并将其添加到池中。

有几个选项可以放置这个方法。最明显的地方是一个轻量级集装箱。或者,您可以创建一个新的工厂类。或者您可以使工厂方法是静态的,并将其放在一个实际的flyweight类中。

结构

Flyweight设计模式的结构 Flyweight设计模式的结构
  1. Flyweight模式只是一个优化。在应用它之前,请确保您的程序存在与内存中同时存在大量相似对象相关的RAM消耗问题。确保这个问题不能以任何其他有意义的方式解决。

  2. 轻量级选手类包含可在多个对象之间共享的原始对象状态的部分。同一个flyweight对象可以用于许多不同的上下文中。存储在flyweight内的状态被称为内在。传递给flyweight的方法的状态被调用外在。

  3. 上下文类包含所有原始对象中唯一的外部状态。当一个上下文与一个flyweight对象配对时,它表示原始对象的完整状态。

  4. 通常,原始对象的行为保持在flyweight类中。在这种情况下,无论谁调用flyweight的方法,都必须将适当的外部状态位传递到方法的参数中。另一方面,可以将该行为移动到上下文类,后者将仅将链接的flyweight用作数据对象。

  5. 客户端计算或存储flyweight的外部状态。从客户端的角度来看,flyweight是一个模板对象,可以通过将一些上下文数据传递到其方法的参数中来在运行时进行配置。

  6. 轻量级的工厂管理现有flyweight的池。使用工厂,客户不会直接创建蝇蝇。相反,他们调用工厂,向它传递所需flyweight的固有状态。工厂查看以前创建的flyweight,并返回一个与搜索条件匹配的现有flyweight,或者如果没有找到就创建一个新的flyweight。

伪代码

在本例中,轻量级选手当在画布上呈现数百万个树对象时,模式有助于减少内存使用。

Flyweight图案示例

模式从主体中提取重复的内在状态类,并将其移动到蝇量级类TreeType

现在不再将相同的数据存储在多个对象中,而是保存在几个轻量级对象中,并链接到适当的对象充当上下文的对象。客户端代码使用flyweight工厂创建新的树对象,该工厂封装了搜索正确对象和在需要时重用它的复杂性。

// flyweight类包含//树状态的一部分。这些字段存储每个//特定树的唯一值。例如,您在这里找不到树//坐标。但是许多//树之间共享的纹理和颜色都在这里。由于此数据通常是大的,因此将其保存在每个树对象中会浪费大量内存。相反,我们//可以将纹理、颜色和其他重复数据提取到一个单独的对象中,许多单独的树对象可以//引用。类TreeType是字段名称字段颜色字段纹理构造函数TreeType(名称,颜色,纹理){…}方法draw(canvas, x, y)是// 1。创建一个给定类型、颜色和纹理的位图。/ / 2。 Draw the bitmap on the canvas at X and Y coords. // Flyweight factory decides whether to re-use existing // flyweight or to create a new object. class TreeFactory is static field treeTypes: collection of tree types static method getTreeType(name, color, texture) is type = treeTypes.find(name, color, texture) if (type == null) type = new TreeType(name, color, texture) treeTypes.add(type) return type // The contextual object contains the extrinsic part of the tree // state. An application can create billions of these since they // are pretty small: just two integer coordinates and one // reference field. class Tree is field x,y field type: TreeType constructor Tree(x, y, type) { ... } method draw(canvas) is type.draw(canvas, this.x, this.y) // The Tree and the Forest classes are the flyweight's clients. // You can merge them if you don't plan to develop the Tree // class any further. class Forest is field trees: collection of Trees method plantTree(x, y, name, color, texture) is type = TreeFactory.getTreeType(name, color, texture) tree = new Tree(x, y, type) trees.add(tree) method draw(canvas) is foreach (tree in trees) do tree.draw(canvas)

适用性

只有当您的程序必须支持大量的对象,而这些对象几乎无法装入可用的RAM时,才使用Flyweight模式。

应用模式的好处很大程度上取决于如何以及在哪里使用它。它在以下情况下最有用:

  • 应用程序需要生成大量类似的对象
  • 这会耗尽目标设备上的所有可用RAM
  • 对象包含重复的状态,可以在多个对象之间提取和共享

如何实施

  1. 将一个将成为flyweight的类的字段分为两部分:

    • 内在状态:包含跨多个对象复制的不变数据的字段
    • 外部状态:包含每个对象特有的上下文数据的字段
  2. 在类中保留表示固有状态的字段,但要确保它们是不可变的。它们应该只在构造函数内部取初始值。

  3. 复习一下使用外部状态字段的方法。对于方法中使用的每个字段,引入一个新参数并使用它而不是字段。

  4. 可选地,创建一个工厂类来管理flyweight池。它应该在创建一个新的flyweight之前检查现有的flyweight。一旦工厂就位,客户必须只通过它请求flyweight。他们应该通过将其固有状态传递给工厂来描述所需的蝇重量。

  5. 客户端必须存储或计算外部状态(上下文)的值才能调用flyweight对象的方法。为了方便起见,外部状态和flyweight引用字段可以移动到单独的上下文类中。

利与弊

  • 假设您的程序有大量类似的对象,您可以节省大量RAM。
  • 当每次有人调用flyweight方法时需要重新计算一些上下文数据时,您可能会在CPU周期上交换RAM。
  • 代码变得更加复杂。新的团队成员总是想知道为什么实体的状态以这样的方式分离。

与其他模式的关系

  • 控件的共享叶节点复合轻量级选手来节省内存。

  • 轻量级选手展示了如何制作许多小对象,然而外观演示如何生成表示整个子系统的单个对象。

  • 轻量级选手就像单例如果您设法将对象的所有共享状态减少到一个flyweight对象。但这些模式之间有两个根本区别:

    1. 应该只有一个单例实例,而轻量级选手类可以有多个具有不同内在状态的实例。
    2. 单例对象可以是可变的。Flyweight对象是不可变的。

代码示例

c#中的Flyweightc++中的Flyweight围棋中的蝇蝇爪哇蝇蝇PHP中的FlyweightPython中的FlyweightRuby中的Flyweight《Rust》中的蝇蝇Swift中的蝇蝇TypeScript中的Flyweight

Baidu
map