iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0

https://ithelp.ithome.com.tw/upload/images/20240924/20168201dM1GBleUGD.png

今天要介紹的是 Flyweight 模式,這也是 GoF 提出的模式之一。

情境

當開發者正在開發應用程式時,發現需要建立大量物件,例如大量視覺元素物件、icon、文字、按鈕等,這些元素會在應用程式的不同地方被經常使用,開發者需要一個有效率處理大量物件的方式。

問題

隨著物件數逐漸增加,記憶體的使用量也逐漸提升,而這些物件包含的資料與其他許多物件非常相似,然而,為每個實例建立和儲存單獨的物件會導致類似數據的冗餘,進而導致效能問題,甚至可能導致記憶體不足,該如何避免使用大量記憶體空間,同時處理大量物件?

權衡

  • 需滿足記憶體使用效率與運算效率:隨著物件數量的增加,需要想辦法節省或有效利用記憶體空間,因為記憶體的資源有限。但是節省太多記憶體可能會導致額外運算成本,因為需要額外的邏輯處理來管理記憶體空間的有效利用,因此需要有辦法能在保持記憶體使用效率的同時,仍確保建立和使用物件不會對系統效能造成太大負擔
  • 需滿足可擴展性與可管理性:當系統建立、處理更多物件時,系統規模也逐漸擴展,此時需維持系統的可擴展性以便日後能繼續發展新功能,但擴展系統會讓系統變更複雜,而影響了可管理性(系統越複雜、規模越大,越難管理),需要有辦法能支援大量物件創建,同時維持系統清晰的管理架構,避免降低可維護性
  • 需滿足通用性與專用性:大量物件間會有許多相似之處,這些共通點可以被提取並共用,但同時這些物件在特定情境下仍有各自獨特的行為與屬性,需要有辦法處理物件的通用性(以減少重複程式碼),同時能保留物件專有特性(以滿足特定情境需求)

解決方案

Flyweight (享元)模式又稱輕量模式,它用於優化重複、緩慢和低效率的共享資料程式碼,可讓相關物件盡可能共享資料,減少應用中的記憶體用量。會稱為「Flyweight」是因為構思此模式的 Paul Calder 和 Mark Linton 兩人當時(1990 年)是以拳擊量級中,輕於 112 磅的拳擊手等級來命名的,代表輕量的意思,因為此模式的目的就是減輕記憶體的用量。

有兩種使用 Flyweight 模式的方式,分為:

  • 資料層:在相似的資料中共享資料,傳統上多屬於此種
  • DOM 應用層:應用 Flyweight 作為中央事件管理器,避免將事件處理附加到具有類似行為的父容器中每個子元素上

因為資料層是比較常見的應用方式,這裡只說明資料層管理的範例;DOM 的應用之後會在其他模式提到,有點類似事件代理,例如將子元素 li 的點擊事件統一綁定到父元素 ul 上,由 ul 作為中央管理器來管理子元素的事件,可減少一一綁定各子元素事件所消耗的記憶體,因此 DOM 應用就先不再詳細說明。

資料層應用

在共享資料時我們會區分為兩種狀態:內在(intrinsic)狀態和外在(extrinsic)狀態,內在狀態是一些物件固有的、內部資訊,也可以把它想成比較不會變動的資訊,例如一顆籃球是圓形的,一般來講正常狀況它的形狀都會固定是圓形,不會因為執行什麼方法而改變它的形狀,那「圓形」這形狀就可稱作籃球的內部狀態;外在狀態則是一些外部資訊與方法,在某些情境下會被改變,例如籃球的位置(座標),籃球位置會隨著比賽進行、球員動作而不斷變化,那「位置」就可稱作籃球的外部狀態。

那我們要如何管理內在狀態與外在狀態?內在狀態可用 Factory 模式來建立單一共享物件,以替換具有相同內在資料的物件,所有物件指存取共享物件資訊即可,這就可以減少要儲存的資料總量,減少記憶體的使用。只有在內在狀態與既有物件不同時,才建立新副本,否則就有現有的。而外在狀態則是使用管理器來處理,方法之一是建立一個管理器物件,讓管理器物件包含中央資料庫儲存所有資料,其中包含外在狀態及他們所屬的 Flyweight 物件。
延續籃球的例子,假設現在我們需要建立大量球類物件,包含各種種類如:排球、籃球、桌球等,這些球的類型、大小可視為內在狀態,而球的顏色、位置、最後被誰使用等我視為外在狀態,因為顏色可能會因情境不同有不同需求(當然視需要也可將顏色當作內在狀態),位置則是因為隨著比賽進行,球的位置會移動。示意圖如下。
https://ithelp.ithome.com.tw/upload/images/20240924/201682011PdLXZ4kNO.jpg
圖 1 球類內外在狀態示意圖(資料來源:自行繪製)

以下為程式碼:

// 建立一個 class,用來創件球的內在狀態
class Ball {
    constructor({ type, size }) {
      this.type = type;
      this.size = size;
      this.shape = 'round';
    }
}


// Flyweight 工廠,用來管理和共享球的內在狀態,如果物件已建立過,就傳回已建立的;否則新建立物件,再傳回
class BallFactory {
    constructor() {
        this.existingBalls = {}; 
    }

    createBall({ type, size }) {
        // 查找特定球是否已存在
        const key = `${type}_${size}`;
        const existingBall = this.existingBalls[key];
        
        if (existingBall) { // 如果存在就回傳已存在的
            return existingBall;
        } else { // 如果沒有就建立新 Ball 並儲存
            const ball = new Ball({ type, size });
            this.existingBalls[key] = ball;
            return ball;
        }
    }
}

const ballRecordDatabase = {}; // 用 ballRecordDatabase 儲存所有球的資料
// 建立管理器將內在狀態與外在狀態合併
class BallRecordManager {
    addBallRecord({
        id,
        type,
        size,
        color,
        position,
        inUse,
        lastUsedBy,
        lastUsedDate
    }) {
        const ballFactory = new BallFactory();
        // 以 ballFactory 的 createBall 來建立球,如果已存在的話就會回傳已存在的資料
        const ball = ballFactory.createBall({ type, size });
        // 加入該球的外部資料,可能同樣的球(如籃球7號)有多個副本,但使用狀態不同
        ballRecordDatabase[id] = {
            color,
            position,
            inUse,
            lastUsedBy,
            lastUsedDate,
            ball
        };
    }
    
    // 以下為操作外部屬性的方法,球類皆可共用這些方法
    updateUsageStatus({
        ballID,
        newStatus,
        lastUsedBy,
        lastUsedDate
    }) {
        const record = ballRecordDatabase[ballID];
        record.inUse = newStatus;
        record.lastUsedBy = lastUsedBy;
        record.lastUsedDate = lastUsedDate;
    }

    changePosition(ballID, newPosition) {
        ballRecordDatabase[ballID].position = newPosition;
    }

    isInUse(ballID) {
        return ballRecordDatabase[ballID].inUse;
    }
    
    getBall(ballID) {
        const ball = ballRecordDatabase[ballID];
        if (!ball) {
            console.log(`Ball with ID ${ballID} not found.`);
            return null;
        }
        return ball;
    }
}

使用方式如下:

// 建立 BallRecordManager 實例,這會管理所有的球類記錄
const ballManager = new BallRecordManager();

// 新建球類
ballManager.addBallRecord({
    id: 'ball1',
    type: 'basketball',
    size: '7',
    color: 'orange',
    position: { x: 10, y: 20 },
    inUse: false,
    lastUsedBy: 'John Doe',
    lastUsedDate: '2024-08-20'
});

ballManager.addBallRecord({
    id: 'ball2',
    type: 'basketball',
    size: '7',
    color: 'yellow',
    position: { x: 15, y: 25 },
    inUse: true,
    lastUsedBy: 'Foo Bar',
    lastUsedDate: '2024-08-19'
});

ballManager.addBallRecord({
    id: 'ball3',
    type: 'football',
    size: '5',
    color: 'white',
    position: { x: 5, y: 30 },
    inUse: false,
    lastUsedBy: 'David Smith',
    lastUsedDate: '2024-08-18'
});

// 更新球的使用狀態
ballManager.updateUsageStatus({
    ballID: 'ball1',
    newStatus: true,
    lastUsedBy: 'Alice',
    lastUsedDate: '2024-08-21'
});

以上面使用範例來說,當我使用 ballManager 來新建球類時,它就會自動處理內在狀態的共享,這樣我就不會重複創建同樣的球類實例,例如同樣都是 basketball 類型且 size7的球,我就可以共享內在狀態,不用再建立新的,可節省記憶體使用。此外,外在狀態可以根據需要而靈活使用,每個球都有各自的記錄資料,可反映目前的使用狀況。這個例子適合需要管理大量相似物件的應用場景,例如體育設備管理系統、遊戲中的道具管理等。

優點

以 Flyweight 作為解決方案優點如下:

  • 可減少記憶體的使用:透過共享內在狀態,當內在狀態屬性都相同時,就能共享狀態,避免記憶體儲存重複的內在狀態
  • 內外在狀態分離,可讓 Flyweight 物件在不同環境被共享

缺點

以 Flyweight 作為解決方案缺點如下:

  • 程式碼複雜度高:開發者需區分物件的內在與外狀態,並建立管理器與工廠來處理對應邏輯,會讓程式碼變得更長、更複雜。以上面範例來說,原本可能只需要一個 Ball 的 class 就可以建立球類物件,但多了其他 class 來管理內外在狀態,讓程式碼較難理解與管理
  • 維護困難:延續上點,因程式碼變得複雜,內外在狀態又分別在不同地方管理,維護上可能會較困難
  • 為了使物件可以共享,Flyweight 模式需要將 Flyweight 物件的狀態外部化,讀取外部狀態的時間變長,以上述例子來說,我們在 addBallRecord 的時候還要透過 ballFactory 去讀取現在是否已存在相同內在狀態的球,會增加執行時間,因此這是犧牲執行速度來換取記憶體

其他補充

在看到 Flyweight 工廠的「如果有就回傳現有的,沒有再建立新的」,不知道大家有沒有聯想到 Singleton 單例模式呢?當我們將物件的所有共享狀態簡化為一個 Flyweight 物件,Flyweight 就和 Singleton 有點類似,但兩者在根本上是不同的,以下為差異:

特性 Singleton Flyweight
實例數量 只會建立一個實例 可以有多個實例,各實例的內在狀態也不同(如足球、籃球等)
可變性 Singleton 物件是可變的 Flyweight 物件是不可變的

提到共享狀態這概念,其實在 JavaScript 中,我們可輕鬆利用原型繼承的方式來處理共享的資料,就不需使用複雜的 Flyweight。此外,現在硬體的記憶體空間通常足夠大,不太需要以 Flyweight 來節省記憶體空間,因此 Flyweight 在現在已沒那麼重要,但多瞭解看看也很好~

Reference


上一篇
[Day 09] Decorator 模式
下一篇
[Day 11] Observer 模式
系列文
30天的 JavaScript 設計模式之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言