本博客大部分内容来于免费在线学习设计模式

1:享元模式

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

2:享元模式问题

在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。

3:享元模式解决方案

在黑白棋子,坐标点这些类中,你会发现一些成员变量储存的值是相同的,而每个粒子的另一些状态(坐标,颜色等)是不同的。

对象的常量数据通常被称为内在状态(即不会随着环境的改变而改变的可共享部分), 其位于对象中,其他对象只能读取但不能修改其数值。而对象的其他状态常常能被其他对象 “从外部” 改变,因此被称为外在状态(随环境改变而改变的不可以共享的部分)。

享元模式建议不在对象中存储外在状态,而是将其传递给依赖于它的一个特殊方法。程序只在对象中保存内在状态,以方便在不同情景下重用。这些对象的区别仅在于其内在状态(与外在状态相比,内在状态的变体要少很多),因此你所需的对象数量会大大削减。

4:享元工厂

为了能更方便地访问各种享元, 你可以创建一个工厂方法来管理已有享元对象的缓存池。 工厂方法从客户端处接收目标享元对象的内在状态作为参数, 如果它能在缓存池中找到所需享元, 则将其返回给客户端; 如果没有找到, 它就会新建一个享元, 并将其添加到缓存池中。

5:享元模式结构

20200902103014

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

代码示例:

模拟黑白棋子的创建

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 简化下棋
public class FlyweightTest {
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
ChessFactory chessFactory = new ChessFactory();
Chessman w = chessFactory.getChess("w");
w.down(i, i);
Chessman b = chessFactory.getChess("b");
b.down(i + 10, i + 10);
}
}
}

// 抽象享元角色:棋子
interface Chessman {

void down(int x, int y);

void show();
}

// 具体享元角色:白棋
class WhiteChess implements Chessman {
private String color;
private int x;
private int y;

@Override
public void down(int x, int y) {
color = "white";
this.x = x;
this.y = y;
show();
}

@Override
public void show() {
System.out.println(color + " " + x + " : " + y);
}
}

// 具体享元角色:黑棋
class BlackChess implements Chessman {
private String color;
private int x;
private int y;

@Override
public void down(int x, int y) {
color = "black";
this.x = x;
this.y = y;
show();
}

@Override
public void show() {
System.out.println(color + " " + x + " : " + y);
}
}

// 享元工厂
class ChessFactory {
private ArrayList<Chessman> list;
private final String WHITE = "w";
private final String BLACK = "b";

public ChessFactory() {
list = new ArrayList<>();
Chessman whiteChess = new WhiteChess();
list.add(whiteChess);
Chessman blackChess = new BlackChess();
list.add(blackChess);
}

public Chessman getChess(String type) {
if (WHITE.equals(type)) {
return list.get(0);
} else if (BLACK.equals(type)) {
return list.get(1);
} else {
return null;
}
}
}

6:享元模式适用场景

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

7:享元模式优缺点

优点 缺点
如果程序中有很多相似对象, 那么你将可以节省大量内存。 你可能需要牺牲执行速度来换取内存, 因为他人每次调用享元方法时都需要重新计算部分情景数据。
代码会变得更加复杂。 团队中的新成员总是会问: ​ “为什么要像这样拆分一个实体的状态?”。

8:享元模式与其他模式关系

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

9:享元模式举例

例如键盘上的键帽,abcdefg这些键帽的模型都是一样,这都是他的内在状态,不会改变。会改变的只有印在键帽上的字母,字母就是外在状态。享元工厂就相当于你的零件仓库,只需要告诉他需要添加哪个按键,如果有这个按键就不操作,但是如果仓库里没有,就添加一个印了这个字母的按钮。

10:参考