当有大量重复物件时,抽离物件相同部分的资料,并用专属的工厂管理,好减少重复的资料,减少记忆体的消耗,避免出现剩余记忆体不足导致程序 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。(计算方式来源)
假设画面要容纳一万棵树,每棵树的差异在 positionX
和 positionY
,那记忆体的使用约为 1 mb 左右,依照现在出场笔电基本配备是 8GB 来看,消耗量算小。
但要长远来看,如果能找出节省记忆体的方法,就有改良的空间了。
现在抽出共同的部分,拆分成 treeType
与 Tree
,则程序码:
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
的部分,一样 positionX
和 positionY
带入不同位置,则记忆体的使用约为 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;
}
}
}
因此,实践的作法是:
具有相同资料的物件: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;
}
}
具有相同资料的物件: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 模式真心觉得不好理解,几本书关於该模式的介绍是:
每个字我都看得懂,组装成句子後就不懂,归咎於没有相关的开发经验,关於「减少重复物件好减少记忆体消耗」的经验,在网页开发上较少琢磨在这一块,如果身在游戏产业或许就懂概念也说不定。
书看不懂就在网路上找寻资源,在这篇文章(连结)内讲的清楚的多,搭配文章提供的 Java AWT 示范程序码(连结),才明白 Flyweight 的初衷。
了解初衷後重新检视整个模式,看到模式的复杂度,连带影响模式的实作,必须满足拥有大量重复物件的情境下,到底要多大量才会对记忆体有巨大的负担?实际测试後发现自己撰写的测试码本身占用的记忆体都不大,索性改成建立大量物件,从中看出使用模式的前後差异。
这篇文章花了我三天的时间才完成,想想就觉得恐怖。
明天将介绍 Structural patterns 的第七个也是该类别最後一个模式:Proxy 模式。
<<: Day 16 — To Do List (3) 深入HTML Service -1
>>: 30天打造品牌特色电商网站 Day.17 微互动设计按钮实作(3)
https://wolkesau.medium.com/ai-machine-learning-aa...
为什麽选择建立 header component 呢? 网站各个页面都会共用 固定版型而且不需要传入...
GitHub:https://github.com/dannypc1628/Angular-Tou...
之前就有好几次想参加铁人赛,但不知道自己有什麽可以写30天的主题。刚好近期完成了硕论,既然研究做了、...
本文内容 阅读官方文件 Angular Components Overview 的笔记内容。 Com...