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

Composite Pattern (Mẫu Composite)

Định nghĩa (Definition)

Composite tổ chức các object thành cấu trúc cây (Tree Structure) để biểu diễn quan hệ phần-toàn thể (Part-Whole Hierarchy). Nó cho phép client xử lý object đơn lẻ và composition một cách thống nhất — item riêng lẻ và nhóm item dùng chung interface.

Ví dụ Coffee Shop

Quán cà phê có menu với item riêng lẻ (Latte, Croissant) và combo (Breakfast Combo, Family Bundle). Cho dù là item đơn hay combo, hệ thống tính giá và hiển thị chi tiết theo cùng một cách.

Cấu trúc (Structure)

Component Interface

public interface IMenuComponent
{
string GetName();
decimal GetPrice();
string Print(string indent = "");
}

Leaf — Item đơn lẻ trên menu (Individual Menu Item)

public class MenuItem : IMenuComponent
{
public string Name { get; }
public decimal Price { get; }

public MenuItem(string name, decimal price)
{
Name = name;
Price = price;
}

public string GetName() => Name;
public decimal GetPrice() => Price;

public string Print(string indent = "") =>
$"{indent}- {Name}: ${Price:F2}";
}

Composite — Combo (Combo Meal)

public class MenuCombo : IMenuComponent
{
public string Name { get; }
private readonly List<IMenuComponent> _children = new();

public MenuCombo(string name)
{
Name = name;
}

public void Add(IMenuComponent component) => _children.Add(component);
public void Remove(IMenuComponent component) => _children.Remove(component);
public IReadOnlyList<IMenuComponent> GetChildren() => _children.AsReadOnly();

public string GetName() => Name;

public decimal GetPrice() => _children.Sum(c => c.GetPrice());

public string Print(string indent = "")
{
var sb = new StringBuilder();
sb.AppendLine($"{indent}+ {Name} (${GetPrice():F2})");
foreach (var child in _children)
sb.AppendLine(child.Print(indent + " "));
return sb.ToString().TrimEnd();
}
}

Client — Xây dựng menu (Building the Menu)

// Item riêng lẻ
var latte = new MenuItem("Latte", 4.50m);
var espresso = new MenuItem("Espresso", 2.50m);
var croissant = new MenuItem("Croissant", 3.00m);
var muffin = new MenuItem("Blueberry Muffin", 3.50m);
var juice = new MenuItem("Fresh Orange Juice", 4.00m);

// Breakfast Combo: Latte + Croissant
var breakfastCombo = new MenuCombo("Breakfast Combo");
breakfastCombo.Add(latte);
breakfastCombo.Add(croissant);

// Family Bundle: 2 Latte + Espresso + 2 Croissant + Juice
var familyBundle = new MenuCombo("Family Bundle");
familyBundle.Add(latte);
familyBundle.Add(latte); // latte thứ hai
familyBundle.Add(espresso);
familyBundle.Add(croissant);
familyBundle.Add(croissant); // croissant thứ hai
familyBundle.Add(juice);

// Combo lồng nhau — "Super Bundle" chứa các combo khác
var superBundle = new MenuCombo("Super Bundle");
superBundle.Add(breakfastCombo);
superBundle.Add(familyBundle);
superBundle.Add(muffin); // item thêm

// In tất cả — item đơn và combo được xử lý giống nhau
var allItems = new IMenuComponent[] { latte, breakfastCombo, familyBundle, superBundle };
foreach (var item in allItems)
{
Console.WriteLine(item.Print());
Console.WriteLine($" Tổng: ${item.GetPrice():F2}");
Console.WriteLine();
}

// Output:
// - Latte: $4.50
// Tổng: $4.50
//
// + Breakfast Combo ($7.50)
// - Latte: $4.50
// - Croissant: $3.00
// Tổng: $7.50
//
// + Family Bundle ($21.50)
// - Latte: $4.50
// - Latte: $4.50
// - Espresso: $2.50
// - Croissant: $3.00
// - Croissant: $3.00
// - Fresh Orange Juice: $4.00
// Tổng: $21.50
//
// + Super Bundle ($32.00)
// + Breakfast Combo ($7.50)
// - Latte: $4.50
// - Croissant: $3.00
// + Family Bundle ($21.50)
// - Latte: $4.50
// - Latte: $4.50
// - Espresso: $2.50
// - Croissant: $3.00
// - Croissant: $3.00
// - Fresh Orange Juice: $4.00
// - Blueberry Muffin: $3.50
// Tổng: $32.00
mẹo

Chú ý cấu trúc cây (Tree Structure): SuperBundle → [BreakfastCombo, FamilyBundle, Muffin]. Combo có thể chứa combo khác — GetPrice()Print() đệ quy xử lý việc lồng nhau một cách tự nhiên.

Transparent vs Safe Composite

Transparent (mặc định)Safe
Quản lý conAdd/Remove trong interfaceChỉ trong MenuComposite
Hành vi LeafLeaf ném NotSupportedExceptionLeaf không có Add/Remove
Đơn giản cho clientClient không cần kiểm tra typeClient phải kiểm tra type trước khi Add/Remove
Đánh đổiClient code đơn giản hơn, rủi ro lỗi runtimeAn toàn hơn, nhưng client cần type checking

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

  • System.Windows.Forms.Control — control chứa child control (Controls collection)
  • WPF Panel — panel chứa child UI element trong cấu trúc cây
  • File system API — directory chứa file và subdirectory
  • IConfigurationSection — configuration section chứa child section

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

  • Cần biểu diễn quan hệ phần-toàn thể (Part-Whole Hierarchy) (menu → item, directory → file)
  • Muốn client bỏ qua sự khác biệt giữa item đơn lẻ và nhóm
  • Cấu trúc tự nhiên đệ quy hoặc dạng cây (Tree-Like)

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

  • Cấu trúc phẳng (không lồng nhau) — list đơn giản là đủ
  • Item đơn lẻ và nhóm có hành vi rất khác — ép chúng vào cùng một interface tạo ra thiết kế gượng gạo
  • Cần type safety cho thao tác add/remove lúc compile time

Điểm chính (Key Takeaways)

  • Composite xử lý item đơn lẻ và nhóm một cách thống nhất thông qua interface chung
  • Cấu trúc cây được xử lý tự nhiên với thao tác đệ quy (Recursive Operations)
  • Thêm loại item mới hoặc loại combo mới không thay đổi client code (OCP)
  • Leaf và composite chia sẻ cùng interface — client code giữ đơn giản

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

Q: Làm sao xử lý thao tác chỉ có ý nghĩa với composite (như Add/Remove)? Hai cách: Transparent — đặt Add/Remove trong interface, leaf ném NotSupportedException. Safe — chỉ đặt Add/Remove trên composite class, bắt buộc client kiểm tra type. Transparent phổ biến hơn trong thực tế.

Q: Composite chỉ là cấu trúc dữ liệu cây (Tree Data Structure) thôi phải không? Cấu trúc cây là một phần, nhưng insight chính là interface thống nhất (Uniform Interface). Không có nó, bạn cần if (item is MenuCombo) ở khắp nơi. Composite loại bỏ việc phân nhánh đó bằng cách làm interface giống hệt nhau.

Q: Composite liên quan đến DOM hay file system như thế nào? Cả hai đều là ví dụ Composite kinh điển. HTML DOM element chứa child element. Directory chứa file và subdirectory. Trong cả hai trường hợp, bạn có thể xử lý một node đơn và một subtree một cách thống nhất.

  • Decorator — cấu trúc tương tự (bao component) nhưng thêm hành vi, không tổng hợp
  • Iterator — duyệt cấu trúc composite mà không phơi bày internals
  • Visitor — thêm thao tác cho composite mà không sửa class
  • SOLID — OCP — loại component mới không làm hỏng code hiện có