今天要介紹的是 Flyweight 模式,這也是 GoF 提出的模式之一。
當開發者正在開發應用程式時,發現需要建立大量物件,例如大量視覺元素物件、icon、文字、按鈕等,這些元素會在應用程式的不同地方被經常使用,開發者需要一個有效率處理大量物件的方式。
隨著物件數逐漸增加,記憶體的使用量也逐漸提升,而這些物件包含的資料與其他許多物件非常相似,然而,為每個實例建立和儲存單獨的物件會導致類似數據的冗餘,進而導致效能問題,甚至可能導致記憶體不足,該如何避免使用大量記憶體空間,同時處理大量物件?
Flyweight (享元)模式又稱輕量模式,它用於優化重複、緩慢和低效率的共享資料程式碼,可讓相關物件盡可能共享資料,減少應用中的記憶體用量。會稱為「Flyweight」是因為構思此模式的 Paul Calder 和 Mark Linton 兩人當時(1990 年)是以拳擊量級中,輕於 112 磅的拳擊手等級來命名的,代表輕量的意思,因為此模式的目的就是減輕記憶體的用量。
有兩種使用 Flyweight 模式的方式,分為:
因為資料層是比較常見的應用方式,這裡只說明資料層管理的範例;DOM 的應用之後會在其他模式提到,有點類似事件代理,例如將子元素 li
的點擊事件統一綁定到父元素 ul
上,由 ul
作為中央管理器來管理子元素的事件,可減少一一綁定各子元素事件所消耗的記憶體,因此 DOM 應用就先不再詳細說明。
在共享資料時我們會區分為兩種狀態:內在(intrinsic)狀態和外在(extrinsic)狀態,內在狀態是一些物件固有的、內部資訊,也可以把它想成比較不會變動的資訊,例如一顆籃球是圓形的,一般來講正常狀況它的形狀都會固定是圓形,不會因為執行什麼方法而改變它的形狀,那「圓形」這形狀就可稱作籃球的內部狀態;外在狀態則是一些外部資訊與方法,在某些情境下會被改變,例如籃球的位置(座標),籃球位置會隨著比賽進行、球員動作而不斷變化,那「位置」就可稱作籃球的外部狀態。
那我們要如何管理內在狀態與外在狀態?內在狀態可用 Factory 模式來建立單一共享物件,以替換具有相同內在資料的物件,所有物件指存取共享物件資訊即可,這就可以減少要儲存的資料總量,減少記憶體的使用。只有在內在狀態與既有物件不同時,才建立新副本,否則就有現有的。而外在狀態則是使用管理器來處理,方法之一是建立一個管理器物件,讓管理器物件包含中央資料庫儲存所有資料,其中包含外在狀態及他們所屬的 Flyweight 物件。
延續籃球的例子,假設現在我們需要建立大量球類物件,包含各種種類如:排球、籃球、桌球等,這些球的類型、大小可視為內在狀態,而球的顏色、位置、最後被誰使用等我視為外在狀態,因為顏色可能會因情境不同有不同需求(當然視需要也可將顏色當作內在狀態),位置則是因為隨著比賽進行,球的位置會移動。示意圖如下。
圖 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
類型且 size
為 7
的球,我就可以共享內在狀態,不用再建立新的,可節省記憶體使用。此外,外在狀態可以根據需要而靈活使用,每個球都有各自的記錄資料,可反映目前的使用狀況。這個例子適合需要管理大量相似物件的應用場景,例如體育設備管理系統、遊戲中的道具管理等。
以 Flyweight 作為解決方案優點如下:
以 Flyweight 作為解決方案缺點如下:
Ball
的 class 就可以建立球類物件,但多了其他 class 來管理內外在狀態,讓程式碼較難理解與管理addBallRecord
的時候還要透過 ballFactory
去讀取現在是否已存在相同內在狀態的球,會增加執行時間,因此這是犧牲執行速度來換取記憶體在看到 Flyweight 工廠的「如果有就回傳現有的,沒有再建立新的」,不知道大家有沒有聯想到 Singleton 單例模式呢?當我們將物件的所有共享狀態簡化為一個 Flyweight 物件,Flyweight 就和 Singleton 有點類似,但兩者在根本上是不同的,以下為差異:
特性 | Singleton | Flyweight |
---|---|---|
實例數量 | 只會建立一個實例 | 可以有多個實例,各實例的內在狀態也不同(如足球、籃球等) |
可變性 | Singleton 物件是可變的 | Flyweight 物件是不可變的 |
提到共享狀態這概念,其實在 JavaScript 中,我們可輕鬆利用原型繼承的方式來處理共享的資料,就不需使用複雜的 Flyweight。此外,現在硬體的記憶體空間通常足夠大,不太需要以 Flyweight 來節省記憶體空間,因此 Flyweight 在現在已沒那麼重要,但多瞭解看看也很好~