0%

译-设计模式-结构模式之Flyweight

目的

Flyweight是结构模式的一种,通过在多个对象见共享对象状态的通用部分而不是让各个对象独自持有的方式来让你在可用的RAM中装入更多的对象。

问题

在长时间工作后想要找些乐趣,你决定写一个简单的视频游戏。玩家能够在地图上移动并且可以相互射击。你决定实现一个真实的粒子系统并让它称为这个游戏的特性。子弹,导弹和爆炸产生的碎片应该到处飞舞并且能够给玩家分配经验。

过了一会,你最后一次提交代码并且把游戏发给了你的朋友,希望他能够立马玩起来。尽管游戏在你电脑上完美运行,但是你的朋友却不能玩起来。这个游戏总是在他的电脑上运行一会儿就崩溃。

在你看了日之后,发现是因为RAM不足导致了游戏崩溃。这个看起来和你的粒子系统有关。

每一个例子由一个包含和丰富数据的对象表示。在某一刻,当团战达到高潮,可用的RAM无法装入新创建的粒子,此时程序就崩溃了。

crash

解决

仔细检查Particle类,你会发现color和sprite数据是对象中最耗内存的字段。更糟糕的是,这些字段存储的是所有粒子都相同的数据。比如,所有的子弹都是相同的颜色和质地。

solution

粒子的其他数据,像坐标,移动向量和速度是所有粒子唯一的。另外,这些字段的数据是实时变化的。对color和sprite这类保留常量而言,它们看起来就像粒子的可变上下文。

这些context-specific对象的变量数据通常被叫做“外在状态”,因为他们早对象外部变化。对象余下的状态,就是这些不可变的数据,被叫做“内在状态”。

Flyweight模式建议你不要再对象内部存储外部状态,而是通过调用方法时当作参数传递进来。通过这种方式你就可以把不可变状态留在对象内,这样你就可以在不同的上下文环境中重用它。更重要的,你将会需要很少这种对象,因为仅仅出现在固有状态不同时,而这种状态并不多。

就我们的游戏而言,只需要三个粒子对象就能满足(子弹,导弹和碎片)。现在你应该猜到了,这种分离对象的方式就叫做“flyweight”(享元,这个术语来自拳击,表示选手小于111磅)。

flyweight

外部状态存储

但是我们在那里做外部状态的移动?用一些类来持有他,对吗?大多情况下,把这些数据移动到容器中是很便利的,该容器用于在应用模式之前聚合对象。

在我们的例子中,它是主要游戏对象。你可以创建额外的数组字段来存储坐标,向量和速度。另外,你也需要其他的数组来存储表示一个粒子的指定享元对象的引用。

等一下!难道我们不需要在一开始就需要有相同数量的上下问对象?从技术上讲,需要。但事实是,这些对象要比之前小太多了。最耗内存的字段现在仅存活在几个享元对象中。上千个上下文对象可以连接并且重用单个享元对象,而不是复制状态到内部各自存储。

extrinsic

不可变性

因为相同的享元对象可以被用在不同的上下文中,你必须确保它们的状态不能被改变。享元应该智能通过构造参数来接收内部状态。它们不应该暴露set方法或者称为公开字段。

享元工厂

你可以创建一个工厂方法来管理已经存在享元对象的池子。这个方法接收客户端期望的内部状态,并能够在已经存在的享元对象中匹配这个状态,如果找到就返回它。如果没有找到,它将创建一个新享元并且把它加入池子。

有几个地方可以放置该方法。通常的做法是放在享元容器中。另外,一个新工厂类应该被创建。你甚至可以把工厂方法做成静态的,并将其放在主要的Flyweight类中。

结构

structure

  1. 不要忘记,Flyweight模式是一种优化,只有在使用大量相似对象的程序中才有意义。模式把对象的状态分成两个部分:享元和上下文。

  2. Flyweight保存能够在多个对象之间共享的原始对象状态的一部分。相同的享元对象可以被用在许多不同的上下文中。被保存在享元中的状态叫做“内部状态”。原始对象状态的另一部分通过参数传递给flyweight的称为“外在状态”。

  3. Context包含外部状态,就是对所有原对象都唯一的那些。当一个上下文和一个享元对象结合起来,它就表示来一个原对象的所有状态。

  4. 大多情况下,原对象的行为保留在Flyweight类中。在这种情况下,不管是谁调用一个享元方法,必须把外部状态通过方法参数传递进来。另一方面,这个行为也可以放在Context类中,把连接的享元仅当作数据对象。

  5. Client计算或者存储享元的外部状态。从一个客户端角度看,一个享元就是一个模版,可以在运行时通过调用带有上下文数据参数的方法进行配置。

  6. Flyweight Factory管理已经存在享元的池子。客户端不直接创建享元。它们调用享元工厂的方法并且告诉这个创建方法它们期望得到享元的内部状态。工厂先去享元池中查找,如果这种享元已经存在就直返回,否则,就创建一个新的。

伪代码

在这个例子中,享元模式帮助在画布上渲染一百万棵树。模式从一个主Tree类中抽离出重复的内部状态并把它放到一个享元类TreeType中。

pseudocode

现在不是将相同数据存储在多个对象中,而是把它们保留在几个享元对象中并且被响应的Tree对象连接(译者注:引用)。客户端代码通过享元工厂和不同的书协作,这个工厂封装了在新树对象中重用现有树类型的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// The Flyweight class contains only a portion of state that describes a tree.
// These field store values that hardly unique for each particular tree. You
// won't find here tree coordinates, however textures and colors shared between
// multiple are here. And since this data is usually BIG, you'd waste a lot of
// memory by keeping it in each tree object. That's why we extract texture,
// colors and other data to a separate flyweight class that can be referenced
// from all those similar trees.
class TreeType is
field name
field color
field texture
constructor Tree(name, color, texture) { ... }
method draw(canvas, x, y) is
Create a bitmap from type, color and texture.
Draw bitmap on canvas at X and Y.

// Flyweight factory decides whether to re-use existing flyweight or 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

// Context object contains extrinsic part of tree state. Application can create
// billions of these since they are pretty thin: just two integer coordinates
// and one reference.
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)

// Tree and Forest classes are Flyweight's clients. You might merge them
// together if you don't plan to develop a 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
tree.draw(canvas)

适用性

  • 当你在给定的RAM中难以装下必须要支持数量级的对象时。

    应用Flyweight模式的好处在很大程度上取决于使用方式和位置。它常用在:

    • 一个需要大量对象的应用;
    • 这些对象占用系统全部RAM;
    • 对象包含重复对象,并且它们可以被抽离并且共享。

如何实现

  1. 将一个类的字段划分享元,从以下两方面入手:

    • 内部状态:字段包含不变的数据,许多对象的该字段值重复;
    • 外部状态:字段包含上下文数据,所有对象的这个字段值都不一样。
  2. 把那些代表内部状态的字段保留在这个类中,并确保它们的不便性。它们应仅仅能通构造方法接收数据。

  3. 将外在状态的字段转化为引用它们的方法的参数。

  4. 创建一个享元工厂类。它应该在创建新享元之间检查该享元是否已经存在。客户端必须从享元工厂中请求享元。客户端需要在获取享元时需要像工厂方法描述它们期望的享元。

  5. 客户端必须存储或计算外部状态(上下文)的值以便能够调用flyweight对象的方法。

优点

  • 节省RAM,因此允许一个程序支持更多对象。

缺点

  • 在查找或者计算上下文时浪费CPU。

  • 创建类更多的额外类增加代码复杂度。

和其他模式的关系

  • Flyweight常常和Composite结合使用来实现叶子结点共享和节约RAM。

  • Flyweight展示如何让创造更多小对象,而Facade告诉我们如何让用一个单独对象来代表一个完整子系统。

  • 当每件事情都减少成一个享元对象时,Flyweight就和Singleton很像了。但是请记住,它们两者之间有两个根本区别:

    1. 单例对象是可变的。享元对象是不可变的。
    2. Singleton的实力职能有一个,而Flyweight类可以有多个不同内部状态的实例。

Java中模式的应用

用例:Flyweight目的单一:最小化内存占用。如果您的程序不会遇到RAM不足,那么你可以暂时忽略此模式。

Java核心库中Flyweight的例子:

  • java.lang.Integer#valueOf(int) (also Boolean, Byte, Character, Short, Long and BigDecimal)

鉴定:Flyweight可以通过创建方法来识别,该方法返回缓存的对象,而不是创建新的对象。

参考

翻译整理自:https://refactoring.guru/design-patterns/flyweight