ข้ามไปยังเนื้อหา

Flyweight

Flyweight ลดการใช้หน่วยความจำให้น้อยที่สุดด้วยการแบ่งปัน state ให้มากที่สุดเท่าที่จะเป็นไปได้ระหว่าง object ที่คล้ายกันจำนวนมาก โดยแยก state ที่สามารถแบ่งปันได้ออกจาก state ที่ต้องคงความเฉพาะตัวในแต่ละการใช้งาน

ลองนึกถึงระบบอนุภาคหรือเกมที่ render ต้นไม้นับพันต้น ต้นไม้แต่ละต้นมีตำแหน่ง แต่ก็มี mesh, texture และสี ที่เป็นข้อมูลหนักที่เหมือนกันในทุกต้นไม้ชนิดเดียวกัน การเก็บข้อมูลทั้งหมดนั้นไว้ในทุก ๆ ต้นจากต้นไม้แสนต้นสิ้นเปลืองหน่วยความจำมหาศาล ทั้งที่จริง ๆ แล้วมีเพียงตำแหน่งเท่านั้นที่แตกต่างกัน

Flyweight แยกข้อมูลของแต่ละ object ออกเป็นสองส่วน intrinsic state คือ mesh, texture, สี ถูกแบ่งปัน เปลี่ยนแปลงไม่ได้ และเก็บไว้ครั้งเดียวต่อหนึ่งชนิดใน object ที่ใช้ร่วมกันตัวเดียว ส่วน extrinsic state คือตำแหน่งของแต่ละ instance ถูกเก็บไว้นอก flyweight และส่งเข้ามาเมื่อจำเป็น เช่น ตอน draw factory แจกจ่าย flyweight โดยคืน object ที่ใช้ร่วมกันตัวเดิมสำหรับ intrinsic key เดียวกันแทนที่จะสร้างตัวใหม่ ต้นไม้แสนต้นจึงอ้างอิงไปยัง TreeType flyweight ที่ใช้ร่วมกันเพียงไม่กี่ตัวบวกกับตำแหน่งน้ำหนักเบาของตัวเอง

classDiagram
  class TreeType {
    +mesh: string
    +texture: string
    +draw(x, y) string
  }
  class TreeTypeFactory {
    -pool: Map
    +get(mesh, texture) TreeType
  }
  class Tree {
    -x: int
    -y: int
    -type: TreeType
    +draw() string
  }
  TreeTypeFactory --> TreeType : caches and returns shared
  Tree --> TreeType : references shared intrinsic state
ต้นไม้จำนวนมากแบ่งปัน TreeType flyweight ไม่กี่ตัว ส่วนตำแหน่งยังคงเป็น extrinsic
  • Flyweight (TreeType) — ถือ intrinsic state ที่แบ่งปันได้และเปลี่ยนแปลงไม่ได้ และรับ extrinsic state เป็น parameter
  • Flyweight Factory (TreeTypeFactory) — cache flyweight ไว้และคืนตัวที่มีอยู่แล้วสำหรับ intrinsic key ที่กำหนด โดยสร้างใหม่เฉพาะเมื่อ miss เท่านั้น
  • Context (Tree) — เก็บ extrinsic state และ reference ไปยัง flyweight ที่ใช้ร่วมกัน
  • Client — ทำงานผ่าน context และ factory โดยไม่เคยทำซ้ำ intrinsic state

ต้นไม้หลายพันต้นแบ่งปันพูลเล็ก ๆ ของ TreeType flyweight ต้นไม้แต่ละต้นเก็บเพียงตำแหน่งของตัวเองและ reference ไปยัง type ที่ใช้ร่วมกัน

class TreeType {
constructor(readonly mesh: string, readonly texture: string) {}
draw(x: number, y: number): string {
return `${this.mesh}/${this.texture} at (${x},${y})`;
}
}
class TreeTypeFactory {
private pool = new Map<string, TreeType>();
get(mesh: string, texture: string): TreeType {
const key = `${mesh}|${texture}`;
let type = this.pool.get(key);
if (type === undefined) {
type = new TreeType(mesh, texture);
this.pool.set(key, type);
}
return type;
}
get size(): number {
return this.pool.size;
}
}
const factory = new TreeTypeFactory();
const trees = Array.from({ length: 1000 }, (_, i) => ({
x: i,
y: i,
type: factory.get('oak', 'bark.png'), // all share one flyweight
}));
console.log(trees[0].type.draw(trees[0].x, trees[0].y)); // oak/bark.png at (0,0)
console.log(factory.size); // 1 — one shared TreeType for 1000 trees
  • ข้อดี: ลดหน่วยความจำลงอย่างมากเมื่อโปรแกรมถือ object จำนวนมหาศาลที่มี state ซ้ำกันเยอะ
  • ข้อดี: การ caching ของ factory ยังสามารถเร่งการสร้างให้เร็วขึ้นด้วยการนำ object ที่มีอยู่กลับมาใช้ใหม่
  • ข้อดี: การรวม intrinsic state ไว้ใน object ที่ใช้ร่วมกันและเปลี่ยนแปลงไม่ได้ตัวเดียวสามารถปรับปรุง cache locality ได้
  • ข้อเสีย: การแยก state ออกเป็น intrinsic และ extrinsic ทำให้ code และ signature ของการเรียกซับซ้อนขึ้น
  • ข้อเสีย: flyweight ต้องเปลี่ยนแปลงไม่ได้เมื่อถูกแบ่งปัน การ mutate state ที่แบ่งปันจะทำให้ทุก context ที่อ้างอิงอยู่เสียหาย
  • ข้อเสีย: pattern นี้คุ้มค่าก็ต่อเมื่ออยู่ในสเกลใหญ่เท่านั้น สำหรับ object เพียงไม่กี่ตัว การบันทึกข้อมูลกินต้นทุนมากกว่าที่ประหยัดได้
  • Singleton แบ่งปัน instance เพียงตัวเดียวเป๊ะ ส่วน Flyweight แบ่งปันพูลเล็ก ๆ ของ instance ที่เปลี่ยนแปลงไม่ได้ซึ่งมี key เป็น intrinsic state
  • Factory Method และ flyweight factory ต่างก็รวมการสร้างไว้ที่ศูนย์กลาง แต่งานที่นิยามตัว flyweight factory คือการคืน object ที่มีอยู่แล้วแทนที่จะสร้างตัวใหม่เสมอ
FlyweightSingletonPrototype
จำนวน instanceหลายตัว (แต่ share state)หนึ่งตัวเท่านั้นหลายตัว (clone จากต้นแบบ)
จุดประสงค์ลด memory ด้วยการ share state ที่ไม่เปลี่ยนควบคุมจำนวน instancecopy object ที่ตั้งค่าแล้ว
stateแบ่งเป็น intrinsic (share) + extrinsic (ไม่ share)มีเพียง instance เดียวcopy ทั้งหมด
เมื่อใช้object จำนวนมากที่มี shared stateต้องการ global access pointobject ที่สร้างยาก/แพง

💡 หมายเหตุสำหรับ developer

Pattern นี้พบได้บ่อยใน:

  • String interning (Java, Python) — string literal ที่ซ้ำกันแชร์ object เดียวกันในหน่วยความจำ
  • Game engine texture/sprite cache — asset ที่ซ้ำกัน (ต้นไม้, กระสุน) แชร์ texture object เดียวกันแทนโหลดซ้ำ
  • หมายเหตุ: React key ไม่ใช่ Flyweight — เป็นแค่ identity hint สำหรับ reconciliation ของ list (ช่วยให้ React จับคู่ element ข้าม render) ไม่ได้แชร์ state ระหว่าง component ส่วน Flyweight คือการแชร์ intrinsic state ที่เปลี่ยนแปลงไม่ได้ข้าม object จำนวนมาก ที่เป็นคนละเรื่องกัน
Flyweight แบ่งปันอะไรระหว่าง object จำนวนมาก?
extrinsic state ถูกเก็บไว้ที่ไหน?
หน้าที่ของ flyweight factory คืออะไร?
ทำไม intrinsic state ที่แบ่งปันจึงต้องเปลี่ยนแปลงไม่ได้?