🔥 核心
享元模式通过共享多个对象所共有的相同状态,减少对象占用的内存。
享元模式的本质其实是缓存。
🙁 问题场景
你是一名游戏地图建模师,复杂搭建游戏地图。
现在,你需要搭建一座森林。森林中有成千上万个棵 树(Tree)
,没棵树具有的属性有 位置(x, y)
、高度(h)
、颜色(color)
、材质(texture)
。你做出了成千上万棵树的对象,搭建出了一座美丽而壮观的森林。
你迫不及待的把地图工程发送给你的朋友,让他体验一下这座森林。尽管地图工程在你的电脑上完美运行,但是,他的电脑在进入地图的一瞬间崩溃了。你连忙查看日志,发现崩溃的原因是「内存容量不足」。当然,一名普通游戏玩家的电脑,可比不上一名专业的游戏地图建模师。
你既不想缩减这座森林的一毫一木,又必须让地图工程能在有限内存中运行。怎么办呢?
🙂 解决方案
享元模式可能会比你想象中的复杂,所以请集中注意力。3.2.1,开始。
享元模式提出了两个概念:「内部状态」与「外部状态」,这是对成员变量的分类。
内部状态:相对固定的信息,它们并不能标识对象,存储在享元对象中。
外部状态:可以改变的信息,它们是每个对象独特的属性,存储在每个原对象中。
我知道这个概念非常抽象,难以理解。不妨动动手,对游戏地图中 树(Tree)
的成员变量进行分类:
位置(x, y)
:每个树对象特有的,是外部状态;
高度(h)
:每个树对象特有的,是外部状态;
颜色(color)
:许多树木会具有相同的颜色,相对固定,是内部状态;
材质(texture)
:许多树木会具有相同的材质,相对固定,是内部状态。
因此,位置与高度(x, y, h)
作为成员变量依旧存储在每个对象中,颜色与材质(color, texture)
则封装到享元类中。
思考一下,你不想减少树对象的数量,那么只能从哪方面入手优化呢?每个对象的大小。现在的每个树对象,都拥有着这么多成员变量,内存能不大吗?而现在,我们把其中的部分成员变量封装到享元类中,就可以直接将原对象中的这几个成员变量替换为享元对象的引用,从而减小了每个对象占用的内存;从整体的角度看,有限个享元对象被成千上万个对象复用,使得享元对象的这部分数据无需重复存储。这就是享元模式优化内存的本质。
理解并消化一下。好了,我们继续引入又一个概念:「享元工厂」。
享元工厂就是一个管理享元对象的缓存池。如果能在缓存池中找到所需享元,则直接返回复用;如果没有找到,就新建一个享元,并将其加入缓存池中。
至此,享元模式已经介绍完毕,或者说,复合享元模式已经介绍完毕。下面接着介绍单纯享元模式。在此之前,我还是建议你读明白上面的内容。
…
…
…
…
好了,现在假设你已经基本理解了上面的内容。
单纯享元模式的思路是,不改变每个对象所占用的内存大小,而是减少对象的数量——内部状态相同的对象,其实是同一个对象的复用,只是这个对象的外部状态不断被更改罢了。
MOBA游戏对英雄地位进行了分类,如刺客、法师、辅助、战士等。现在,英雄工厂中已经有一个刺客英雄对象,你将其拿出来,对其属性进行了相应的修改,便得到了一个新刺客英雄。虽然新刺客英雄诞生了,但第一个刺客英雄却不复存在了。
因此,单纯享元模式所能应用的场景是比较局限的。比如上面的森林地图,你不能为了节省内存,在一个树对象的基础上修改属性得到另一个树对象,这种复用会使得你的森林中只有光秃秃的几棵树。
🌈 有趣的例子
因为上面这个“搭建森林”的例子有点儿复杂,所以我们干脆写出代码,帮助我们彻底理解享元模式。
树
class Tree {
private int x;
private int y;
private int h;
private Type type;
public Tree(int x, int y, int h, Type type) {
this.x = x;
this.y = y;
this.h = h;
this.type = type;
}
public void plant() {
System.out.println("种下一棵树~");
System.out.println("坐标x: " + x);
System.out.println("坐标y: " + y);
System.out.println("高度: " + h);
System.out.println("颜色: " + type.getColor());
System.out.println("材质: " + type.getTextrue());
}
}
享元类
class Type {
// 依次为绿色、黄色、褐色、黑色
static final String[] COLORS = {"green", "yellow", "brown", "black"};
// 依次为杉木、橡木、桦木、槐木
static final String[] TEXTURES = {"fir", "oak", "birch", "locust"};
private String color;
private String textrue;
public Type(String color, String textrue) {
this.color = color;
this.textrue = textrue;
}
public String getColor() { return color; }
public String getTextrue() { return textrue; }
}
享元工厂
class TypeFactory {
private Map<String, Type> typeCache = new HashMap<>();
public Type getType(String color, String textrue) {
String key = color + textrue;
if (!typeCache.containsKey(key)) {
typeCache.put(key, new Type(color, textrue));
}
return typeCache.get(key);
}
}
森林
class Forest {
public List<Tree> trees = new ArrayList<>();
public void init() {
TypeFactory typeFactory = new TypeFactory();
// 搭建1000棵树
// 每棵树具有随机的位置,随机的高度,随机的颜色,随机的材质
for (int i = 0; i < 1000; i++) {
Type type = typeFactory.getType(initColor(), initTexture());
Tree tree = new Tree(initX(), initY(), initH(), type);
tree.plant();
trees.add(tree);
}
}
private int initX() {
return (int) (Math.random() * 10000);
}
private int initY() {
return (int) (Math.random() * 10000);
}
private int initH() {
return (int) (Math.random() * 10);
}
private String initColor() {
return Type.COLORS[(int) (Math.random() * Type.COLORS.length)];
}
private String initTexture() {
return Type.TEXTURES[(int) (Math.random() * Type.TEXTURES.length)];
}
}
public class FlyweightPatternDemo {
public static void main(String[] args) {
// 初始化森林
// 即种植树木
new Forest().init();
}
}
种下一棵树~
坐标x: 9198
坐标y: 8446
高度: 6
颜色: green
材质: oak
种下一棵树~
坐标x: 6862
坐标y: 9088
高度: 1
颜色: green
材质: birch
种下一棵树~
坐标x: 7010
坐标y: 6307
高度: 0
颜色: black
材质: locust
种下一棵树~
坐标x: 3470
坐标y: 9363
高度: 5
颜色: brown
材质: birch
种下一棵树~
坐标x: 1287
坐标y: 9743
高度: 2
颜色: black
材质: fir
种下一棵树~
坐标x: 5547
坐标y: 403
高度: 8
颜色: black
材质: oak
...1000 more
☘️ 使用场景
◾️仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。
应用该模式所获的收益大小取决于使用它的方式和情景。它在下列情况中最有效:程序需要生成数量巨大的相似对象;这将耗尽目标设备的所有内存;对象中包含可抽取且能在多个对象间共享的重复状态。
🧊 实现方式
(1)将需要改写为享元的类成员变量拆分为两个部分:
- 内在状态:包含不变的、可在许多对象中重复使用的数据的成员变量。
- 外在状态:包含每个对象各自不同的情景数据的成员变量
(2)保留类中表示内在状态的成员变量,并将其属性设置为不可修改。这些变量仅可在构造函数中获得初始数值。
(3)找到所有使用外在状态成员变量的方法,为在方法中所用的每个成员变量新建一个参数,并使用该参数代替成员变量。
(4)你可以有选择地创建工厂类来管理享元缓存池,它负责在新建享元时检查已有的享元。如果选择使用工厂,客户端就只能通过工厂来请求享元,它们需要将享元的内在状态作为参数传递给工厂。
(5)客户端必须存储和计算外在状态(情景)的数值,因为只有这样才能调用享元对象的方法。为了使用方便,外在状态和引用享元的成员变量可以移动到单独的情景类中。
🎲 优缺点
➕ 如果程序中有很多相似对象,那么你将可以节省大量内存。
➕ 需要缓冲池的场景。
➖ 你可能需要牺牲执行速度来换取内存,因为他人每次调用享元方法时都需要重新计算部分情景数据。
➖ 代码会变得更加复杂。团队中的新成员总是会问:“为什么要像这样拆分一个实体的状态?”。
🌸 补充
复合享元模式/单纯享元模式是设计模式中的难点。
所以请耐心理解与体会 >_<。