享元模式(学习笔记)

  1. 意图

  运用共享技术有效的支持大量细粒度的对象

  2. 动机

   假设开发了一款简单的游戏:玩家们在地图上移动并进行相互射击。大量的子弹、导弹和爆炸弹片在整个地图上穿行,为玩家提供紧张刺激的游戏体验。但是,运行了几分钟后,游戏因为内存容量不足而发生了崩溃。研究发现,每个粒子(一颗子弹、 一枚导弹或一块弹片)都由包含完整数据的独立对象来表示。当玩家在游戏中鏖战进入高潮后的某一时刻,游戏将无法在剩余内存中载入新建粒子,于是程序就崩溃了

  

  仔细观察粒子Particle类,会注意到颜色(color)和精灵图(sprite)这两个成员变量所消耗的内存要比其他变量多得多。另外,对所有例子来说,这两个成员变量所存储的数据几乎完全一样 (比如所有子弹的颜色和精灵图都一样)。每个粒子的另一些状态 (坐标 移动矢量和速度则是不同的,因为这些成员变量的数值会不断变化。内在状态存储于flyweight中,它包含了独立于场景的信息,这些信息使得flyweight可以被共享。而外部状态取决于flyweight场景,并根据场景而变化,因此不可共享。用户对象负责在必要的时候将外部状态传递给Flyweight

    

 

  3. 适用性

  • 程序需要生成巨大的相似对象,以至于消耗目标对象的所有内存
  • 对象中包含可抽取且能在多个对象间共享的重复状态

  4. 结构

 

  5. 效果

  使用FlyWeight模式时,传输、查找和/或计算外部状态都会产生运行时开销,尤其当flyweight原先被存储为内部状态时。然而,空间上的节省抵消了这些开销。共享的flyweight越多,空间节省越大

  存储节约由以下因素决定:

  1. 由于共享带来的实例总数减少的数量

  2. 对象内部状态的平均数量

  3. 外部状态是计算的还是存储的

  共享的flyweight越多,存储节约的也就越多。节约量随着共享状态的增多而增大。当对象使用大量的内部及外部状态,并且外部状态是计算出来的而非存储的时候,节约量将达到最大

  6. 代码实现  

  如果渲染一片森林 (1,000,000 棵树)!每棵树都由包含一些状态的对象来表示 (坐标和纹理等)。 尽管程序能够完成其主要工作,但很显然它需要消耗大量内存。因为,太多树对象包含重复数据 (名称、 纹理和颜色)。因此我们可用享元模式来将这些数值存储在单独的享元对象中 (Tree­Type类)。现在我们不再将相同数据存储在数千个 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 和 Big­Decimal)

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

posted @ 2021-07-27 16:17  慕仙白  阅读(127)  评论(0编辑  收藏  举报