Day 16: Structural patterns - Flyweight

目的

当有大量重复物件时,抽离物件相同部分的资料,并用专属的工厂管理,好减少重复的资料,减少记忆体的消耗,避免出现剩余记忆体不足导致程序 Crash。

说明

现在有个情境,建立一个游戏画面,画面内有许多树木。

树木的基本资料是:

class Tree {
  constructor(positionX, positionY) {
    this.name = '榕树';
    this.leafColor = '#2D5A27';
    this.trunkColor = '#A56406';
    this.positionX = positionX;
    this.positionY = positionY;
  }
}

用 Node.js 内建的 process.memoryUsage() 计算的话,一棵数目在 heap 的消耗约为 0.11 kb。(计算方式来源

假设画面要容纳一万棵树,每棵树的差异在 positionXpositionY,那记忆体的使用约为 1 mb 左右,依照现在出场笔电基本配备是 8GB 来看,消耗量算小。

但要长远来看,如果能找出节省记忆体的方法,就有改良的空间了。

现在抽出共同的部分,拆分成 treeTypeTree,则程序码:

const treeType = {
  name: '榕树',
  leafColor: '#2D5A27',
  trunkColor: '#A56406',
};

class Tree {
  constructor(positionX, positionY) {
    this.positionX = positionX;
    this.positionY = positionY;
  }
}

一样用 process.memoryUsage() 计算,则一棵数目在 heap 的消耗约为 0.09 kb。
而一万棵 Tree 的部分,一样 positionXpositionY 带入不同位置,则记忆体的使用约为 0.87 mb 左右

使用的记忆体下降了,这就是 Flyweight 想要达成的事情。

假如现在除了榕树之外呢?能加入樟树、茄苳和台湾栾树吗?此时会建立工厂,负责管理这些树种,程序码可以这样写:

class TreeType {
  constructor(name, leafColor, trunkColor) {
    this.name = name;
    this.leafColor = leafColor;
    this.trunkColor = trunkColor;
  }
}

class TreeTypeFactory {
  constructor() {
    this.treeTypeMap = new Map();
  }

  getTreeType(treeType, leafColor, trunkColor) {
    if (this.treeTypeMap.has(treeType)) {
      return this.treeTypeMap.get(treeType);
    } else {
      const newTreeType = new TreeType(treeType, leafColor, trunkColor);
      this.treeTypeMap.set(treeType, newTreeType);
      return newTreeType;
    }
  }
}

因此,实践的作法是:

  1. 观察程序本身有没有大量「重复使用」且「差异不大」的物件,找出相同的资料,视为不变动的部分(intrinsic states)。
  2. 建立工厂,负责建立、管理能够共用的部分,以物件的形式储存。
  3. 索取共用资料时,给予 Key 即可取得,并依情况给予变动的资料(extrinsic states),或是使用另一个物件组合变动的资料与共用资料物件。

UML 图

Flyweight Pattern UML Diagram

使用 Java 实作

具有相同资料的物件:TreeType(Flyweight 物件)

public class TreeType {
    private String name;
    private String leafColor;
    private String trunkColor;

    public TreeType(String name, String leafColor, String trunkColor) {
        this.name = name;
        this.leafColor = leafColor;
        this.trunkColor = trunkColor;
    }

    public String getName() {
        return name;
    }

    public String getLeafColor() {
        return leafColor;
    }

    public String getTrunkColor() {
        return trunkColor;
    }
}

负责管理、建立相同物件的工厂:TreeTypeFactory(Flyweight 工厂)

public class TreeTypeFactory {
    private static HashMap<String, TreeType> treeTypes = new HashMap<>();

    public static TreeType getTreeType(String name, String leafColor, String trunkColor) {
        TreeType result = treeTypes.get(name);

        if (result == null) {
            result = new TreeType(name, leafColor, trunkColor);
            treeTypes.put(name, result);
        }

        return result;
    }

    public static int getTreeTypesCounts() {
        return treeTypes.size();
    }
}

使用相同物件的物件:Tree

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 int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public TreeType getType() {
        return type;
    }
}

测试,建立一组由三种树木组合而成的森林:ForestFlyweightSample

public class ForestFlyweightSample {
    private static Random random = new Random();
    private static ArrayList<Tree> forest = new ArrayList<>();
    private static final long MEGABYTE = 1024L * 1024L;

    public static void main(String[] args) {
        System.out.println("建立一棵榕树");
        TreeTypeFactory.getTreeType("榕树", "#2D5A27", "#A56406");

        System.out.println("\n建立一棵樟树");
        TreeTypeFactory.getTreeType("樟树", "#296223", "#915A08");

        System.out.println("\n建立一颗台湾乐树");
        TreeTypeFactory.getTreeType("台湾乐树", "#174A11", "#492C00");

        System.out.println("\n建立四千棵榕树");
        for (int i = 0; i < 4000; i++) {
            Tree tree = createTree("榕树");
            forest.add(tree);
        }

        System.out.println("\n建立四千棵樟树");
        for (int i = 0; i < 4000; i++) {
            Tree tree = createTree("樟树");
            forest.add(tree);
        }

        System.out.println("\n建立四千颗台湾乐树");
        for (int i = 0; i < 4000; i++) {
            Tree tree = createTree("台湾乐树");
            forest.add(tree);
        }

        System.out.println("\n这片森林,拥有" + forest.size() + "颗树木");
        System.out.println("TreeTypeFactory 有 " + TreeTypeFactory.getTreeTypesCounts() + " 颗树种");
        calculateRAMUsage();
    }

    private static Tree createTree(String treeType) {
        // 1 - 12000
        int positionX = random.nextInt(12000 + 1) + 1;
        int positionY = random.nextInt(12000 + 1) + 1;
        Tree tree = null;

        if (treeType.equals("榕树")) {
            tree = new Tree(positionX, positionY, TreeTypeFactory.getTreeType("榕树", "#2D5A27", "#A56406"));
        } else if (treeType.equals("樟树")) {
            tree = new Tree(positionX, positionY, TreeTypeFactory.getTreeType("樟树", "#296223", "#915A08"));
        } else if (treeType.equals("台湾乐树")) {
            tree = new Tree(positionX, positionY, TreeTypeFactory.getTreeType("台湾乐树", "#174A11", "#492C00"));
        }

        return tree;
    }

    private static void calculateRAMUsage() {
        // 取得 Java runtime
        Runtime runtime = Runtime.getRuntime();

        // 执行 garbage collector
        runtime.gc();

        // 计算记忆体的使用
        long memory = runtime.totalMemory() - runtime.freeMemory();
        System.out.println("Used memory is bytes: " + memory);
        System.out.println("Used memory is megabytes: " + bytesToMegabytes(memory));
    }

    private static long bytesToMegabytes(long bytes) {
        return bytes / MEGABYTE;
    }
}

使用 JavaScript 实作

具有相同资料的物件:TreeType(Flyweight 物件)

class TreeType {
  /**
   * @param {string} name
   * @param {string} leafColor
   * @param {string} trunkColor
   */
  constructor(name, leafColor, trunkColor) {
    this.name = name;
    this.leafColor = leafColor;
    this.trunkColor = trunkColor;
  }

  getName() {
    return this.name;
  }

  getLeafColor() {
    return this.leafColor;
  }

  getTrunkLeaf() {
    return this.trunkColor;
  }
}

负责管理、建立相同物件的工厂:TreeTypeFactory(Flyweight 工厂)

class TreeTypeFactory {
  constructor() {
    /** @type Map<string, TreeType> */
    this.treeTypes = new Map();
  }

  /**
   * @param {string} name
   * @param {string} leafColor
   * @param {string} trunkColor
   * @returns TreeType
   */
  getTreeType(name, leafColor, trunkColor) {
    if (this.treeTypes.has(name)) {
      return this.treeTypes.get(name);
    } else {
      const result = new TreeType(name, leafColor, trunkColor);
      this.treeTypes.set(name, result);
      return result;
    }
  }

  getTreeTypesCounts() {
    return this.treeTypes.size;
  }
}

使用相同物件的物件:Tree

class Tree {
  /**
   * @param {number} x
   * @param {number} y
   * @param {TreeType} type
   */
  constructor(x, y, type) {
    this.x = x;
    this.y = y;
    this.type = type;
  }

  getX() {
    return this.x;
  }

  getY() {
    return this.y;
  }

  getType() {
    return this.type;
  }
}

测试,建立一组由三种树木组合而成的森林:ForestFlyweightSample

/**
 * @param {string} treeType
 * @param {TreeTypeFactory} treeTypeFactory
 * @returns Tree
 */
const createTree = (treeType, treeTypeFactory) => {
  // 1 - 12000
  const positionX = Math.random() * (12000 - 1) + 1;
  const positionY = Math.random() * (12000 - 1) + 1;
  let tree = null;

  switch (treeType) {
    case '榕树':
      tree = new Tree(positionX, positionY, treeTypeFactory.getTreeType("榕树", "#2D5A27", "#A56406"));
      break;
    case '樟树':
      tree = new Tree(positionX, positionY, treeTypeFactory.getTreeType("樟树", "#296223", "#915A08"));
      break;
    case '台湾乐树':
      tree = new Tree(positionX, positionY, treeTypeFactory.getTreeType("台湾乐树", "#174A11", "#492C00"));
      break;
  }

  return tree;
}

const calculateRAMUsage = () => {
  // 计算记忆体的使用
  const used = process.memoryUsage();
  console.log("Used memory is bytes: " + used.heapUsed);
  console.log("Used memory is megabytes: " + Math.round((used.heapUsed / 1024 / 1024) * 100) / 100);
}

const forestFlyweightSample = () => {
  const forest = [];
  const treeTypeFactory = new TreeTypeFactory();

  console.log("建立一棵榕树");
  treeTypeFactory.getTreeType("榕树", "#2D5A27", "#A56406");

  console.log("\n建立一棵樟树");
  treeTypeFactory.getTreeType("樟树", "#296223", "#915A08");

  console.log("\n建立一颗台湾乐树");
  treeTypeFactory.getTreeType("台湾乐树", "#174A11", "#492C00");

  console.log("\n建立四千棵榕树");
  for (let i = 0; i < 4000; i++) {
    const tree = createTree("榕树", treeTypeFactory);
    forest.push(tree);
  }

  console.log("\n建立四千棵樟树");
  for (let i = 0; i < 4000; i++) {
    const tree = createTree("樟树", treeTypeFactory);
    forest.push(tree);
  }

  console.log("\n建立四千颗台湾乐树");
  for (let i = 0; i < 4000; i++) {
    const tree = createTree("台湾乐树", treeTypeFactory);
    forest.push(tree);
  }

  console.log("\n这片森林,拥有" + forest.length + "颗树木");
  console.log("TreeTypeFactory 有 " + treeTypeFactory.getTreeTypesCounts() + " 颗树种");
  calculateRAMUsage();
}

forestFlyweightSample();

总结

Flyweight 模式真心觉得不好理解,几本书关於该模式的介绍是:

  • 以共享机制有效地支援一大堆小规模的物件。 - 物件导向设计模式。
  • 想让某个类别的一个实体,能够提供最多的「虚拟实体」 - 深入浅出设计模式。
  • 运用共有技术有效地支援大量细粒度的物件 - 大话设计模式。
  • 大量物件共享一些共同性质,降低系统的负荷 -7天学会设计模式。

每个字我都看得懂,组装成句子後就不懂,归咎於没有相关的开发经验,关於「减少重复物件好减少记忆体消耗」的经验,在网页开发上较少琢磨在这一块,如果身在游戏产业或许就懂概念也说不定。

书看不懂就在网路上找寻资源,在这篇文章(连结)内讲的清楚的多,搭配文章提供的 Java AWT 示范程序码(连结),才明白 Flyweight 的初衷。

了解初衷後重新检视整个模式,看到模式的复杂度,连带影响模式的实作,必须满足拥有大量重复物件的情境下,到底要多大量才会对记忆体有巨大的负担?实际测试後发现自己撰写的测试码本身占用的记忆体都不大,索性改成建立大量物件,从中看出使用模式的前後差异。

这篇文章花了我三天的时间才完成,想想就觉得恐怖。

我的库存啊

明天将介绍 Structural patterns 的第七个也是该类别最後一个模式:Proxy 模式。


<<:  Day 16 — To Do List (3) 深入HTML Service -1

>>:  30天打造品牌特色电商网站 Day.17 微互动设计按钮实作(3)

AI & machine learning 组别

https://wolkesau.medium.com/ai-machine-learning-aa...

【Vue】建立 第一个 component | 专案实作

为什麽选择建立 header component 呢? 网站各个页面都会共用 固定版型而且不需要传入...

[影片]第27天:英雄指南-5. 新增应用内导航(2)

GitHub:https://github.com/dannypc1628/Angular-Tou...

Day 31 - 完赛心得

之前就有好几次想参加铁人赛,但不知道自己有什麽可以写30天的主题。刚好近期完成了硕论,既然研究做了、...

新新新手阅读 Angular 文件 - Component - Day21

本文内容 阅读官方文件 Angular Components Overview 的笔记内容。 Com...