设计模式之享元模式

设计模式之享元模式

意图

享元模式 (Flyweight) 是一种结构型设计模式,它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

适用场景

  • 仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。

结构

img

结构说明

  1. 享元模式只是一种优化。 在应用该模式之前, 你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题, 并且确保该问题无法使用其他更好的方式来解决。
  2. 享元 (Flyweight) 类包含原始对象中部分能在多个对象中共享的状态。 同一享元对象可在许多不同情景中使用。 享元中存储的状态被称为 “内在状态”。 传递给享元方法的状态被称为 “外在状态”。
  3. 情景 (Context) 类包含原始对象中各不相同的外在状态。 情景与享元对象组合在一起就能表示原始对象的全部状态。
  4. 通常情况下, 原始对象的行为会保留在享元类中。 因此调用享元方法必须提供部分外在状态作为参数。 但你也可将行为移动到情景类中, 然后将连入的享元作为单纯的数据对象。
  5. 客户端 (Client) 负责计算或存储享元的外在状态。 在客户端看来, 享元是一种可在运行时进行配置的模板对象, 具体的配置方式为向其方法中传入一些情景数据参数。
  6. 享元工厂 (Flyweight Factory) 会对已有享元的缓存池进行管理。 有了工厂后, 客户端就无需直接创建享元, 它们只需调用工厂并向其传递目标享元的一些内在状态即可。 工厂会根据参数在之前已创建的享元中进行查找, 如果找到满足条件的享元就将其返回; 如果没有找到就根据参数新建享元。

结构代码范式

Flyweight : 它是所有具体享元类的超类或接口,通过这个接口,Flyweight 可以接受并作用于外部状态。

1
2
3
abstract class Flyweight {
public abstract void operation(int extrinsicstates);
}

ConcreteFlyweight : 是继承 Flyweight 超类或实现 Flyweight 接口,并为内部状态增加存储空间。

1
2
3
4
5
6
class ConcreteFlyweight extends Flyweight {
@Override
public void operation(int extrinsicstates) {
System.out.println("共享的Flyweight : " + extrinsicstates);
}
}

UnsharedConcreteFlyweight : 指那些不需要共享的 Flyweight 子类,因为 Flyweight 接口共享成为可能,但它并不强制共享。

1
2
3
4
5
6
class UnsharedConcreteFlyweight extends Flyweight {
@Override
public void operation(int extrinsicstates) {
System.out.println("不共享的Flyweight : " + extrinsicstates);
}
}

FlywightFactory :是一个享元工厂,用来创建并管理 Flyweight 对象。它主要是用来确保合理地共享 Flyweight ,当用户请求一个 Flyweight 时, FlyweightFactory 对象提供一个已创建的实例或创建一个(如果对象不存在的话)。

1
2
3
4
5
6
7
8
9
10
11
12
13
class FlywightFactory {
private Hashtable<String, Flyweight> flyweights = new Hashtable<String, Flyweight>();

public FlywightFactory() {
flyweights.put("X", new ConcreteFlyweight());
flyweights.put("Y", new ConcreteFlyweight());
flyweights.put("Z", new ConcreteFlyweight());
}

public Flyweight getFlyweight(String key) {
return ((Flyweight)flyweights.get(key));
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FlyweightPattern {
public static void main(String[] args) {
int extrinsicstates = 1;
FlywightFactory factory = new FlywightFactory();

Flyweight fx = factory.getFlyweight("X");
fx.operation(extrinsicstates);

Flyweight fy = factory.getFlyweight("Y");
fy.operation(++extrinsicstates);

Flyweight fz = factory.getFlyweight("Z");
fz.operation(++extrinsicstates);

Flyweight uf = new UnsharedConcreteFlyweight();
uf.operation(++extrinsicstates);
}
}

输出

1
2
3
4
共享的Flyweight : 1
共享的Flyweight : 2
共享的Flyweight : 3
不共享的Flyweight : 4

伪代码

img

在本例中, 享元模式能有效减少在画布上渲染数百万个树状对象时所需的内存。

该模式从主要的 Tree 类中抽取内在状态, 并将其移动到享元类 树种类Tree­Type 之中。

最初程序需要在多个对象中存储相同数据, 而现在仅需在几个享元对象中保存数据, 然后在作为情景的 对象中连入享元即可。 客户端代码使用享元工厂创建树对象并封装搜索指定对象的复杂行为, 并能在需要时复用对象。

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
// 享元类包含一个树的部分状态。这些成员变量保存的数值对于特定树而言是唯一
// 的。例如,你在这里找不到树的坐标。但这里有很多树木之间所共有的纹理和颜
// 色。由于这些数据的体积通常非常大,所以如果让每棵树都其进行保存的话将耗
// 费大量内存。因此,我们可将纹理、颜色和其他重复数据导出到一个单独的对象
// 中,然后让众多的单个树对象去引用它。
class TreeType is
field name
field color
field texture
constructor TreeType(name, color, texture) { ... }
method draw(canvas, x, y) is
// 1. 创建特定类型、颜色和纹理的位图。
// 2. 在画布坐标 (X,Y) 处绘制位图。

// 享元工厂决定是否复用已有享元或者创建一个新的对象。
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

// 情景对象包含树状态的外在部分。程序中可以创建数十亿个此类对象,因为它们
// 体积很小:仅有两个整型坐标和一个引用成员变量。
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)和森林(Forest)类是享元的客户端。如果不打算继续对树类进行开
// 发,你可以将它们合并。
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)

案例

使用示例: 享元模式只有一个目的: 将内存消耗最小化。 如果你的程序没有遇到内存容量不足的问题, 则可以暂时忽略该模式。

享元模式在核心 Java 程序库中的示例:

识别方法: 享元可以通过构建方法来识别, 它会返回缓存对象而不是创建新的对象。

与其他模式的关系

  • 你可以使用享元模式实现组合模式树的共享叶节点以节省内存。
  • 享元展示了如何生成大量的小型对象, 外观模式则展示了如何用一个对象来代表整个子系统。
  • 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元就和单例模式类似了。 但这两个模式有两个根本性的不同。
    1. 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。 享元对象是不可变的。

参考资料