Chuyển tới nội dung chính

Flyweight Pattern (Mẫu Flyweight)

Định nghĩa (Definition)

Flyweight giảm thiểu việc sử dụng bộ nhớ bằng cách chia sẻ nhiều dữ liệu nhất có thể với các object tương tự khác. Nó tách intrinsic (dùng chung, immutable) state khỏi extrinsic (duy nhất theo context) state, cho phép nhiều object tái sử dụng cùng một dữ liệu dùng chung.

Ví dụ Coffee Shop

Một quán cà phê đông đúc xử lý hàng ngàn đơn mỗi ngày. Mỗi đơn có loại cà phê với thuộc tính dùng chung (tên, giá cơ bản, công thức, mức rang). Thay vì lưu trữ chi tiết này trong mỗi đơn, chúng ta chia sẻ một object CoffeeType cho tất cả đơn cùng loại.

Cấu trúc (Structure)

Flyweight — Intrinsic state dùng chung (Shared Intrinsic State)

// Intrinsic state — dùng chung cho tất cả đơn cùng loại cà phê
public class CoffeeTypeFlyweight
{
public string Name { get; }
public decimal BasePrice { get; }
public string RoastLevel { get; }
public IReadOnlyList<string> Recipe { get; }

public CoffeeTypeFlyweight(string name, decimal price, string roast, List<string> recipe)
{
Name = name;
BasePrice = price;
RoastLevel = roast;
Recipe = recipe.AsReadOnly();
}

public string GetDetails() =>
$"{Name} (${BasePrice:F2}, {RoastLevel}) — {string.Join(" → ", Recipe)}";
}

Flyweight Factory — Cache & Tái sử dụng (Reuse)

public class CoffeeTypeFactory
{
private readonly Dictionary<string, CoffeeTypeFlyweight> _cache = new();

public CoffeeTypeFlyweight GetCoffeeType(string name)
{
if (_cache.TryGetValue(name, out var existing))
return existing;

var flyweight = name switch
{
"Espresso" => new CoffeeTypeFlyweight("Espresso", 2.50m, "Dark",
new() { "Xay 18g hạt", "Tamp", "Chiết xuất 25 giây" }),
"Latte" => new CoffeeTypeFlyweight("Latte", 4.00m, "Medium",
new() { "Rút shot espresso", "Hâm nóng sữa", "Rót sữa" }),
"Cappuccino" => new CoffeeTypeFlyweight("Cappuccino", 3.75m, "Medium",
new() { "Rút shot espresso", "Hâm nóng sữa", "Thêm bọt" }),
"Cold Brew" => new CoffeeTypeFlyweight("Cold Brew", 3.50m, "Light",
new() { "Ngâm hạt 12 giờ", "Lọc", "Phục vụ với đá" }),
_ => throw new ArgumentException($"Loại cà phê không xác định: {name}")
};

_cache[name] = flyweight;
return flyweight;
}

public int CacheSize => _cache.Count;
}

Context — Extrinsic state cho mỗi đơn (Per Order)

// Extrinsic state — duy nhất cho mỗi đơn
public class CoffeeOrder
{
public string CustomerName { get; }
public CupSize Size { get; }
public CoffeeTypeFlyweight CoffeeType { get; } // tham chiếu dùng chung
public decimal MilkCost { get; }

private static readonly Dictionary<CupSize, decimal> _sizeMarkup = new()
{
[CupSize.Small] = 0.0m,
[CupSize.Medium] = 0.50m,
[CupSize.Large] = 1.00m,
};

public CoffeeOrder(string customer, CupSize size, CoffeeTypeFlyweight coffeeType, decimal milkCost = 0)
{
CustomerName = customer;
Size = size;
CoffeeType = coffeeType;
MilkCost = milkCost;
}

public decimal GetTotal() =>
CoffeeType.BasePrice + _sizeMarkup[Size] + MilkCost;

public string PrintOrder() =>
$" {CustomerName}: {Size} {CoffeeType.Name} — ${GetTotal():F2}";
}

Client — Xử lý đơn hàng (Processing Orders)

var factory = new CoffeeTypeFactory();

// 1000 đơn — nhưng chỉ 4 CoffeeTypeFlyweight object trong bộ nhớ
var orders = new List<CoffeeOrder>
{
new("Alice", CupSize.Large, factory.GetCoffeeType("Latte"), 0.60m),
new("Bob", CupSize.Small, factory.GetCoffeeType("Espresso")),
new("Carol", CupSize.Medium, factory.GetCoffeeType("Cappuccino"), 0.50m),
new("Dave", CupSize.Large, factory.GetCoffeeType("Cold Brew")),
new("Eve", CupSize.Medium, factory.GetCoffeeType("Latte")), // tái sử dụng Latte của Alice
new("Frank", CupSize.Small, factory.GetCoffeeType("Espresso")), // tái sử dụng Espresso của Bob
};

Console.WriteLine($"Số loại cà phê trong cache: {factory.CacheSize}");
// Số loại cà phê trong cache: 4

foreach (var order in orders)
Console.WriteLine(order.PrintOrder());

// Output:
// Alice: Large Latte — $5.60
// Bob: Small Espresso — $2.50
// Carol: Medium Cappuccino — $4.75
// Dave: Large Cold Brew — $4.50
// Eve: Medium Latte — $4.50
// Frank: Small Espresso — $2.50

// Kiểm tra rằng tham chiếu dùng chung thực sự là cùng một object
var latte1 = factory.GetCoffeeType("Latte");
var latte2 = factory.GetCoffeeType("Latte");
Console.WriteLine($"\nCùng object Latte? {ReferenceEquals(latte1, latte2)}");
// Cùng object Latte? True
mẹo

Với 1000 đơn Latte, vẫn chỉ có một CoffeeTypeFlyweight cho "Latte" trong bộ nhớ. Công thức, mức rang, và giá cơ bản được lưu trữ một lần và chia sẻ cho tất cả 1000 đơn.

Tác động bộ nhớ (Memory Impact)

Không có Flyweight (1000 đơn)Có Flyweight
Dữ liệu cà phê1000 bản sao tên, giá, công thức1 object dùng chung cho mỗi loại (4 tổng cộng)
Dữ liệu mỗi đơnMọi thứ bị nhân đôiChỉ customer, size, tham chiếu
Chuỗi công thức (Recipe Strings)1000 × danh sách công thức4 × danh sách công thức
Tiết kiệm bộ nhớ~90% cho dữ liệu loại cà phê

Sử dụng thực tế trong .NET (.NET Real-World Usage)

  • string.Intern() — string interning pool của CLR là flyweight cho string
  • char trong text rendering — font chia sẻ glyph data, mỗi ký tự chỉ lưu vị trí
  • Connection pooling — connection dùng chung được tái sử dụng giữa các request
  • ImageSource / cached image — một image chia sẻ giữa nhiều UI control

Khi nào sử dụng (When to Use)

  • App sử dụng số lượng lớn object tương tự
  • Hầu hết state có thể làm extrinsic (chuyển ra ngoài object dùng chung)
  • Object identity không quan trọng — nhiều tham chiếu có thể trỏ đến cùng instance
  • Bộ nhớ là điểm nghẽn (đo lường được, không phải dự đoán)

Khi nào KHÔNG sử dụng (When NOT to Use)

  • Số lượng object ít — overhead của factory không đáng
  • Object không chia sẻ nhiều state chung — không tiết kiệm được gì
  • Tối ưu hóa premature — chỉ áp dụng sau khi profiling cho thấy vấn đề bộ nhớ
  • Object cần mutable — flyweight intrinsic state bắt buộc immutable

Điểm chính (Key Takeaways)

  • Flyweight tách intrinsic (dùng chung, immutable) khỏi extrinsic (per-context) state
  • Factory quản lý cache và trả về instance dùng chung
  • Tất cả flyweight object phải immutable — nếu không state dùng chung sẽ bị hỏng
  • Đây là tối ưu hóa hiệu năng (Performance Optimization), không phải thay đổi cấu trúc — đo lường trước khi áp dụng

Câu hỏi phỏng vấn (Interview Questions)

Q: Chuyện gì xảy ra nếu làm flyweight state mutable? Tất cả object chia sẻ flyweight đó sẽ thấy sự thay đổi. Nếu flyweight Latte của Alice đổi BasePrice thành $10, mọi đơn Latte khác lập tức có giá $10. Flyweight intrinsic state bắt buộc immutable.

Q: string.Intern() có phải flyweight không? Có. CLR duy trì pool string duy nhất. string.Intern() trả về instance chuẩn, nên string giống nhau chia sẻ cùng bộ nhớ. Đây là flyweight kinh điển.

Q: Flyweight khác caching như thế nào? Caching lưu kết quả tính toán để tránh tính lại. Flyweight chia sẻ state giống hệt nhau để tránh trùng lặp. Chúng chồng lấp — flyweight factory thực chất là cache của object dùng chung — nhưng mục đích khác: caching là về tốc độ, flyweight là về bộ nhớ.

  • Composite — cấu trúc cây có thể hưởng lợi từ flyweight cho leaf node
  • Factory Method — flyweight factory dùng factory method bên trong
  • Singleton — ý tưởng "single instance" tương tự, nhưng flyweight quản lý nhiều loại dùng chung
  • Memory Management — hiểu GC và bộ nhớ giúp quyết định khi nào flyweight đáng giá