享元模式(学习笔记)
1. 意图
运用共享技术有效的支持大量细粒度的对象
2. 动机
假设开发了一款简单的游戏:玩家们在地图上移动并进行相互射击。大量的子弹、导弹和爆炸弹片在整个地图上穿行,为玩家提供紧张刺激的游戏体验。但是,运行了几分钟后,游戏因为内存容量不足而发生了崩溃。研究发现,每个粒子(一颗子弹、 一枚导弹或一块弹片)都由包含完整数据的独立对象来表示。当玩家在游戏中鏖战进入高潮后的某一时刻,游戏将无法在剩余内存中载入新建粒子,于是程序就崩溃了
仔细观察粒子Particle类,会注意到颜色(color)和精灵图(sprite)这两个成员变量所消耗的内存要比其他变量多得多。另外,对所有例子来说,这两个成员变量所存储的数据几乎完全一样 (比如所有子弹的颜色和精灵图都一样)。每个粒子的另一些状态 (坐标、 移动矢量和速度)则是不同的,因为这些成员变量的数值会不断变化。内在状态存储于flyweight中,它包含了独立于场景的信息,这些信息使得flyweight可以被共享。而外部状态取决于flyweight场景,并根据场景而变化,因此不可共享。用户对象负责在必要的时候将外部状态传递给Flyweight
3. 适用性
- 程序需要生成巨大的相似对象,以至于消耗目标对象的所有内存
- 对象中包含可抽取且能在多个对象间共享的重复状态
4. 结构
5. 效果
使用FlyWeight模式时,传输、查找和/或计算外部状态都会产生运行时开销,尤其当flyweight原先被存储为内部状态时。然而,空间上的节省抵消了这些开销。共享的flyweight越多,空间节省越大
存储节约由以下因素决定:
1. 由于共享带来的实例总数减少的数量
2. 对象内部状态的平均数量
3. 外部状态是计算的还是存储的
共享的flyweight越多,存储节约的也就越多。节约量随着共享状态的增多而增大。当对象使用大量的内部及外部状态,并且外部状态是计算出来的而非存储的时候,节约量将达到最大
6. 代码实现
如果渲染一片森林 (1,000,000 棵树)!每棵树都由包含一些状态的对象来表示 (坐标和纹理等)。 尽管程序能够完成其主要工作,但很显然它需要消耗大量内存。因为,太多树对象包含重复数据 (名称、 纹理和颜色)。因此我们可用享元模式来将这些数值存储在单独的享元对象中 (TreeType类)。现在我们不再将相同数据存储在数千个 Tree对象中,而是使用一组特殊的数值来引用其中一个享元对象。客户端代码不会知道任何事情, 因为重用享元对象的复杂机制隐藏在了享元工厂中
trees/Tree.java: 包含每棵树的独特状态
package flyweight.trees; import java.awt.*; /** * @author GaoMing * @date 2021/7/19 - 22:14 */ public class Tree { private int x; private int y; private TreeType type; public Tree(int x, int y, TreeType type) { this.x = x; this.y = y; this.type = type; } public void draw(Graphics g) { type.draw(g, x, y); } }
trees/TreeType.java: 包含多棵树共享的状态
package flyweight.trees; import java.awt.*; /** * @author GaoMing * @date 2021/7/19 - 22:14 */ public class TreeType { private String name; private Color color; private String otherTreeData; public TreeType(String name, Color color, String otherTreeData) { this.name = name; this.color = color; this.otherTreeData = otherTreeData; } public void draw(Graphics g, int x, int y) { g.setColor(Color.BLACK); g.fillRect(x - 1, y, 3, 5); g.setColor(color); g.fillOval(x - 5, y - 10, 10, 10); } }
trees/TreeFactory.java: 封装创建享元的复杂机制
package flyweight.trees; import java.awt.*; import java.util.HashMap; import java.util.Map; /** * @author GaoMing * @date 2021/7/19 - 22:15 */ public class TreeFactory { static Map<String, TreeType> treeTypes = new HashMap<>(); public static TreeType getTreeType(String name, Color color, String otherTreeData) { TreeType result = treeTypes.get(name); if (result == null) { result = new TreeType(name, color, otherTreeData); treeTypes.put(name, result); } return result; } }
forest/Forest.java: 我们绘制的森林
package flyweight.forest; import flyweight.trees.TreeFactory; import flyweight.trees.TreeType; import flyweight.trees.Tree; import javax.swing.*; import java.awt.*; import java.util.ArrayList; /** * @author GaoMing * @date 2021/7/19 - 22:16 */ public class Forest extends JFrame { private List<Tree> trees = new ArrayList<>(); public void plantTree(int x, int y, String name, Color color, String otherTreeData) { TreeType type = TreeFactory.getTreeType(name, color, otherTreeData); Tree tree = new Tree(x, y, type); trees.add(tree); } @Override public void paint(Graphics graphics) { for (Tree tree : trees) { tree.draw(graphics); } } }
Demo.java: 客户端代码
package flyweight; import java.awt.*; import flyweight.forest.Forest; /** * @author GaoMing * @date 2021/7/19 - 22:17 */ public class Demo { static int CANVAS_SIZE = 500; static int TREES_TO_DRAW = 1000000; static int TREE_TYPES = 2; public static void main(String[] args) { Forest forest = new Forest(); for (int i = 0; i < Math.floor(TREES_TO_DRAW / TREE_TYPES); i++) { forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE), "Summer Oak", Color.GREEN, "Oak texture stub"); forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE), "Autumn Oak", Color.ORANGE, "Autumn Oak texture stub"); } forest.setSize(CANVAS_SIZE, CANVAS_SIZE); forest.setVisible(true); System.out.println(TREES_TO_DRAW + " trees drawn"); System.out.println("---------------------"); System.out.println("Memory usage:"); System.out.println("Tree size (8 bytes) * " + TREES_TO_DRAW); System.out.println("+ TreeTypes size (~30 bytes) * " + TREE_TYPES + ""); System.out.println("---------------------"); System.out.println("Total: " + ((TREES_TO_DRAW * 8 + TREE_TYPES * 30) / 1024 / 1024) + "MB (instead of " + ((TREES_TO_DRAW * 38) / 1024 / 1024) + "MB)"); } private static int random(int min, int max) { return min + (int) (Math.random() * ((max - min) + 1)); } }
运行结果
1000000 trees drawn --------------------- Memory usage: Tree size (8 bytes) * 1000000 + TreeTypes size (~30 bytes) * 2 --------------------- Total: 7MB (instead of 36MB)
7. 与其他模式的关系
- 可以使用享元模式实现组合模式树的共享叶节点以节省内存
- 享元展示了如何生成大量的小型对象,外观模式则展示了如何用一个对象来代表整个子系统
- 如果你能将对象的所有共享状态简化为一个享元对象,那么享元就和单例模式类似了。但这两个模式有两个根本性的不同:
1. 只会有一个单例实体,但是享元类可以有多个实体,各实体的内在状态也可以不同
2. 单例对象可以是可变的。享元对象是不可变的
8. 已知应用
java.lang.Integer#valueOf(int) (以及 Boolean、Byte、Character、Short、Long 和 BigDecimal)
识别方法:享元可以通过构建方法来识别,它会返回缓存对象而不是创建新的对象