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 หนึ่ง)
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างระบบวาดตัวอักษร (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 (รูปร่างตัวอักษร) และต้อง immutablepublic 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 (ตำแหน่ง สี) พร้อมอ้างอิง flyweightpublic 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