當有大量重複物件時,抽離物件相同部分的資料,並用專屬的工廠管理,好減少重複的資料,減少記憶體的消耗,避免出現剩餘記憶體不足導致程式 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 模式。