iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Software Development

也該是時候學學 Design Pattern 了系列 第 16

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 15: Structural patterns - Facade
下一篇
Day 17: Structural patterns - Proxy
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言