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

Flyweight

ใช้​การ share เพื่อ​รองรับ object ละเอียด​จำนวน​มหาศาล​อย่าง​ประหยัด​หน่วย​ความ​จำ

ลอง​นึก​ภาพ text editor ที่​ต้อง​แสดง​ตัว​อักษร​นับ​ล้าน​ตัว​บน​หน้า​จอ ถ้า​ทุก​ตัว​อักษร (ทุก glyph) เป็น object ที่​เก็บ font outline, font metrics, การ​อ้างอิง bitmap ฯลฯ ของ​ตัวเอง​ทั้งหมด หน่วย​ความ​จำ​จะ​บวม​ขึ้น​มหาศาล​ทั้ง​ที่​ตัว​อักษร 'A' ทุก​ตัว​ใน​เอกสาร​มี​รูปร่าง​เหมือน​กัน​ทุก​ประการ ต่าง​กัน​แค่ ตำแหน่ง ที่​มัน​ถูกวาด

Flyweight เป็น pattern เชิง​โครงสร้าง​ที่​แก้​ปัญหา​นี้​ด้วย​หลักการ การ share (sharing): แยก state ของ object ออก​เป็น​สอง​ประเภท

  • intrinsic state — ข้อมูล​ที่ ไม่​ขึ้น​กับ context, คงที่, และ share กัน​ได้​ระหว่าง​หลาย ๆ การ​ใช้งาน (เช่น รูปร่าง​ตัว​อักษร 'A') — เก็บ​ไว้​ใน flyweight object ตัว​เดียว
  • extrinsic state — ข้อมูล​ที่ ขึ้น​กับ context, เปลี่ยน​ไป​ใน​แต่ละ​ครั้ง​ที่​ใช้ (เช่น ตำแหน่ง x, y หรือ​สี) — client เป็น​ผู้​คำนวณ​แล้ว​ส่ง​เข้า​มา​ตอน​เรียก method แทนที่​จะ​เก็บ​ไว้​ใน object

ผลลัพธ์​คือ​แทนที่​จะ​มี object นับ​ล้าน​ตัว​ที่​ซ้ำ​ข้อมูล​กัน เรา​มี flyweight ที่ share กัน​เพียง​หยิบ​มือหนึ่ง (เช่น glyph ของ​ตัว​อักษร A-Z, a-z, 0-9 ฯลฯ) บวก​กับ​ข้อมูล extrinsic ที่​เบา​มาก ๆ ต่อ​การ​ใช้งาน​หนึ่ง​ครั้ง โดย​ทั่วไป​จะ​มี factory คอย​ดูแล cache ของ flyweight ที่ share อยู่ เพื่อ​คืน instance เดิม​แทน​การ​สร้าง​ใหม่​ทุก​ครั้ง

classDiagram
    class FlyweightFactory {
      -Dictionary cache
      +GetFlyweight(key) Flyweight
    }
    class Flyweight {
      <<interface>>
      +Operation(extrinsicState)
    }
    class ConcreteFlyweight {
      -intrinsicState
      +Operation(extrinsicState)
    }
    class Context {
      -extrinsicState
      -Flyweight flyweight
      +Render()
    }
    class Client
    FlyweightFactory ..> ConcreteFlyweight : creates and caches
    Flyweight <|.. ConcreteFlyweight
    Context o-- Flyweight
    Client --> FlyweightFactory
    Client --> Context
  • Flyweight — interface ที่​ประกาศ method ซึ่ง​รับ extrinsic state เป็น parameter
  • ConcreteFlyweight — เก็บ​เฉพาะ intrinsic state ที่ share ได้ ต้อง immutable เพื่อ​ความ​ปลอดภัย​เมื่อ​ถูก share
  • FlyweightFactory — สร้าง​และ​ดูแล cache/pool ของ flyweight คืน instance เดิม​ถ้า​มี​อยู่​แล้ว​ตาม key
  • Context (หรือ client เอง) — เก็บ extrinsic state และ​อ้างอิง​ไป​ยัง flyweight ที่​เกี่ยวข้อง
  • Client — ขอ flyweight จาก factory แล้ว​ส่ง extrinsic state เข้าไป​ตอน​เรียก​ใช้งาน

เมื่อ client ต้องการ object ใหม่ มัน​จะ​ขอ​ผ่าน FlyweightFactory แทน​การ​สร้าง​เอง factory จะ​เช็ค​ว่า​มี flyweight ที่​ตรง​กับ key (เช่น​ตัว​อักษร หรือ​ประเภท​ของ object) อยู่​ใน cache แล้ว​หรือ​ไม่ ถ้า​มี​ก็​คืนตัว​เดิม ถ้า​ไม่มี​ก็​สร้าง​ใหม่​แล้ว​เก็บ​เข้า cache ก่อน​คืน​กลับ ทุก​ครั้ง​ที่​ต้อง​ใช้งาน​จริง (เช่น​วาด​ตัว​อักษร​บน​หน้า​จอ) client จะ​ส่ง extrinsic state (ตำแหน่ง สี ฯลฯ) เข้าไป​พร้อม​กับ​เรียก method บน flyweight นั้น

sequenceDiagram
    participant Client
    participant Factory as FlyweightFactory
    participant FW as Flyweight A

    Client->>Factory: GetFlyweight(A)
    alt ยังไม่มีใน cache
        Factory->>FW: new ConcreteFlyweight(A)
        Factory-->>Factory: cache.Add(A, FW)
    end
    Factory-->>Client: FW (instance เดิมถ้ามี)
    Client->>FW: Operation(x, y, color)

จุด​สำคัญ​คือ flyweight เอง​ไม่รู้จัก extrinsic state เลย — มัน​แค่​รับ​มา​ใช้​ชั่วคราว​ตอน​ถูก​เรียก จึง​ทำให้ flyweight object เดียวกัน​ถูก​ใช้​ซ้ำ​ได้​พร้อม​กัน​ใน​หลาย context โดย​ไม่​ชน​กัน (เพราะ​ไม่มี mutable state ส่วนตัว​ที่​ผูก​กับ context ใด context หนึ่ง)

ตัวอย่าง​ระบบ​วาด​ตัว​อักษร (glyph) ใน​ระบบ text rendering: Glyph เก็บ​เฉพาะ​รูปร่าง​ตัว​อักษร (intrinsic) ส่วน​ตำแหน่ง​และ​สี​ถูก​ส่ง​เข้า​มา​ตอน Draw

// Flyweight interface — method รับ extrinsic state เป็น parameter เสมอ
public interface IGlyph
{
void Draw(int x, int y, string color);
}
// ConcreteFlyweight — เก็บเฉพาะ intrinsic state (รูปร่างตัวอักษร) และต้อง immutable
public sealed class Glyph : IGlyph
{
private readonly char _character;
private readonly string _fontFamily;
public Glyph(char character, string fontFamily)
{
_character = character;
_fontFamily = fontFamily;
}
public void Draw(int x, int y, string color)
{
// ในของจริงตรงนี้จะไปเรียก rendering engine
// รูปร่างตัวอักษร (_character, _fontFamily) มาจาก intrinsic state
// ตำแหน่งและสีมาจาก extrinsic state ที่ client ส่งเข้ามา
Console.WriteLine($"วาด '{_character}' ({_fontFamily}) ที่ ({x},{y}) สี {color}");
}
}
// FlyweightFactory — ดูแล cache คืน instance เดิมถ้ามี key ซ้ำ
public sealed class GlyphFactory
{
private readonly Dictionary<string, Glyph> _cache = new();
public Glyph GetGlyph(char character, string fontFamily)
{
var key = $"{character}:{fontFamily}";
if (!_cache.TryGetValue(key, out var glyph))
{
glyph = new Glyph(character, fontFamily);
_cache[key] = glyph;
}
return glyph; // อาจเป็น instance เดิมที่ตัวอักษรอื่นใช้อยู่แล้ว
}
public int CachedCount => _cache.Count;
}
// Context — เก็บ extrinsic state (ตำแหน่ง สี) พร้อมอ้างอิง flyweight
public sealed class CharacterInstance
{
private readonly Glyph _glyph;
public int X { get; }
public int Y { get; }
public string Color { get; }
public CharacterInstance(Glyph glyph, int x, int y, string color)
{
_glyph = glyph;
X = x;
Y = y;
Color = color;
}
public void Render() => _glyph.Draw(X, Y, Color);
}
// การใช้งาน: เอกสารมีตัวอักษรนับพัน แต่มี Glyph จริงแค่ไม่กี่สิบตัว
var factory = new GlyphFactory();
var document = new List<CharacterInstance>();
string text = "Hello Flyweight";
int x = 0;
foreach (char c in text)
{
var glyph = factory.GetGlyph(c, "Sarabun"); // share ตัวเดิมถ้าเจอตัวอักษรซ้ำ
document.Add(new CharacterInstance(glyph, x, y: 0, color: "black"));
x += 10;
}
foreach (var ch in document)
ch.Render();
Console.WriteLine($"Glyph object ทั้งหมดใน cache: {factory.CachedCount}"); // น้อยกว่าจำนวนตัวอักษรในเอกสารมาก

จุด​สังเกต: document.Count เท่ากับ​จำนวน​ตัว​อักษร​ทั้งหมด แต่ factory.CachedCount เท่ากับ​จำนวน​ตัว​อักษรไม่​ซ้ำเท่านั้น — นี่​คือ​การ​ประหยัด​หน่วย​ความ​จำ​ที่ flyweight มอบ​ให้

  • application ต้อง​สร้าง object จำนวน​มหาศาล (หลัก​พัน​ถึง​หลัก​ล้าน) ที่​มี​ลักษณะ​คล้าย​กัน
  • ต้นทุน​หน่วย​ความ​จำ​ของ​การ​เก็บ state ซ้ำ ๆ ใน​ทุก object สูง​จน​กระทบ​ประสิทธิภาพ​จริง (วัด​แล้ว​เจอ​ปัญหา ไม่ใช่​แค่​คาด​เดา)
  • สามารถ​แยก state ของ object ออก​เป็น intrinsic (คงที่ share ได้) และ extrinsic (เปลี่ยน​ตาม​การ​ใช้งาน) ได้​อย่าง​ชัดเจน
  • object ส่วน​ใหญ่​ที่​ใช้งาน​สามารถ​ถูก​แทนที่​ด้วย​กลุ่ม object ที่ share กัน​จำนวน​น้อย​กว่า​มาก เมื่อ extrinsic state ถูก​ดึง​ออก​ไป
  • ตัวตน (identity) ของ object ไม่​สำคัญ​ต่อ client — client สนใจ​แค่​พฤติกรรม/ข้อมูล ไม่​สน​ว่า​เป็น instance ไหน
  • object ใน​ระบบ​มี​จำนวน​น้อย หรือ​แต่ละ​ตัว​มี state เฉพาะ​ตัว​มาก​จน​แยก intrinsic/extrinsic ไม่​คุ้ม
  • ยัง​ไม่มี​ปัญหา​หน่วย​ความ​จำ​จริง — การ​เพิ่ม​ความ​ซับซ้อน​เพื่อ optimize ล่วงหน้า​ขัด​กับ​หลัก YAGNI
  • code ที่​ได้​จะ​ซับซ้อน​ขึ้น​มาก​จน​ลด​ความ​สามารถ​ใน​การ​อ่าน​และ​ดูแล​รักษา​โดย​ไม่​ได้​ประโยชน์​คุ้ม​ค่า
  • ต้องการ​ให้​แต่ละ object มี identity หรือ mutable state ส่วนตัว​ที่​ไม่​ควร​ถูก share กับ​ใคร
ด้านรายละเอียด
ข้อดีลด​การ​ใช้​หน่วย​ความ​จำ​ได้​อย่าง​มาก​เมื่อ​มี object ซ้ำ​จำนวน​มาก
ข้อดีรวม state ที่ share ไว้​ที่​เดียว ทำให้​แก้ไข/อัปเดต​ข้อมูล​กลาง​ได้​ง่าย​ขึ้น
ข้อ​เสียcode ซับซ้อน​ขึ้น​ชัดเจน ต้อง​แยก​และ​ส่ง​ผ่าน extrinsic state อย่าง​ระมัดระวัง
ข้อ​เสียอาจ​แลก CPU cycle เพิ่ม​ขึ้น เพราะ​ต้อง​คำนวณ/ส่ง extrinsic state ซ้ำ​ทุก​ครั้ง​ที่​เรียก​ใช้
ข้อ​เสียflyweight ต้อง immutable อย่าง​เคร่งครัด — bug จาก mutable intrinsic state จะ​กระทบ​ทุก context ที่ share instance นั้น
  • Factory Method — flyweight factory มัก​ใช้​แนวคิด​เดียวกัน​ใน​การ​สร้าง​และ​คืน instance
  • Singleton — แนวคิด​เรื่อง instance ที่​ถูก share และ​เข้าถึง​จาก​ส่วนกลาง​คล้าย​กัน แต่ singleton จำกัด​ไว้​ที่​หนึ่ง​ตัว​เท่านั้น ต่าง​จาก flyweight ที่​มี​ได้​หลาย​ตัว​ตาม key
  • Proxy — เป็น pattern เชิง​โครงสร้าง​ที่​ควบคุม​การ​เข้าถึง object เช่น​เดียว​กับ​ที่ flyweight ควบคุม​การ share object
  • Composite — มัก​ถูก​ใช้​ร่วม​กับ flyweight เมื่อ leaf node ใน​โครงสร้าง​ต้นไม้​มี​จำนวน​มาก​และ​ซ้ำ​กัน
  • Decorator — เป็น pattern เชิง​โครงสร้าง​อีก​แบบ​ที่​ห่อ object เพื่อ​เพิ่ม​พฤติกรรม แทนที่​จะ share state
  • DRY — หลักการ​เบื้องหลัง​การ​ไม่​ทำซ้ำ​ข้อมูล​ที่ flyweight นำ​มา​ประยุกต์​ใช้​ใน​ระดับ object