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

Visitor Pattern (Mẫu Visitor)

Định nghĩa (Definition)

Visitor đại diện cho một thao tác được thực hiện trên các phần tử của object structure. Nó cho phép định nghĩa thao tác mới mà không thay đổi class của phần tử mà nó tác động.

Ví dụ Coffee Shop

Menu có nhiều loại item: CoffeeItem, PastryItem, và ComboItem. Muốn tạo các báo cáo khác nhau — báo cáo giá, báo cáo calo, và danh sách nguyên liệu — mà không thêm method vào mỗi loại item. Mỗi báo cáo là một Visitor.

Cấu trúc (Structure)

Element Interface & Concrete Element

public interface IMenuElement
{
void Accept(IMenuVisitor visitor);
}

public class CoffeeItem : IMenuElement
{
public string Name { get; }
public decimal Price { get; }
public int Calories { get; }
public List<string> Ingredients { get; }

public CoffeeItem(string name, decimal price, int calories, List<string> ingredients)
{
Name = name; Price = price; Calories = calories; Ingredients = ingredients;
}

public void Accept(IMenuVisitor visitor) => visitor.Visit(this);
}

public class PastryItem : IMenuElement
{
public string Name { get; }
public decimal Price { get; }
public int Calories { get; }
public List<string> Ingredients { get; }

public PastryItem(string name, decimal price, int calories, List<string> ingredients)
{
Name = name; Price = price; Calories = calories; Ingredients = ingredients;
}

public void Accept(IMenuVisitor visitor) => visitor.Visit(this);
}

public class ComboItem : IMenuElement
{
public string Name { get; }
public List<IMenuElement> Items { get; }

public ComboItem(string name, List<IMenuElement> items)
{
Name = name; Items = items;
}

public void Accept(IMenuVisitor visitor)
{
visitor.Visit(this);
foreach (var item in Items)
item.Accept(visitor);
}
}

Visitor Interface & Concrete Visitor

public interface IMenuVisitor
{
void Visit(CoffeeItem item);
void Visit(PastryItem item);
void Visit(ComboItem item);
}

public class PriceReportVisitor : IMenuVisitor
{
private decimal _total;

public void Visit(CoffeeItem item)
{
Console.WriteLine($" {item.Name,-20} ${item.Price:F2}");
_total += item.Price;
}

public void Visit(PastryItem item)
{
Console.WriteLine($" {item.Name,-20} ${item.Price:F2}");
_total += item.Price;
}

public void Visit(ComboItem item) =>
Console.WriteLine($" [{item.Name}]");

public void PrintTotal() =>
Console.WriteLine($" {\"TỔNG\",-20} ${_total:F2}");
}

public class CalorieReportVisitor : IMenuVisitor
{
private int _total;

public void Visit(CoffeeItem item)
{
Console.WriteLine($" {item.Name,-20} {item.Calories} kcal");
_total += item.Calories;
}

public void Visit(PastryItem item)
{
Console.WriteLine($" {item.Name,-20} {item.Calories} kcal");
_total += item.Calories;
}

public void Visit(ComboItem item) =>
Console.WriteLine($" [{item.Name}]");

public void PrintTotal() =>
Console.WriteLine($" {\"TỔNG\",-20} {_total} kcal");
}

Client

var menu = new IMenuElement[]
{
new CoffeeItem("Latte", 4.00m, 190, new() { "Espresso", "Steamed Milk" }),
new PastryItem("Croissant", 3.00m, 230, new() { "Flour", "Butter" }),
new ComboItem("Breakfast Combo", new List<IMenuElement>
{
new CoffeeItem("Drip Coffee", 3.00m, 5, new() { "Coffee Grounds" }),
new PastryItem("Muffin", 3.50m, 400, new() { "Flour", "Blueberries" }),
}),
};

Console.WriteLine("=== Báo cáo giá ===");
var priceVisitor = new PriceReportVisitor();
foreach (var item in menu) item.Accept(priceVisitor);
priceVisitor.PrintTotal();

Console.WriteLine("\n=== Báo cáo calo ===");
var calorieVisitor = new CalorieReportVisitor();
foreach (var item in menu) item.Accept(calorieVisitor);
calorieVisitor.PrintTotal();
mẹo

Thêm báo cáo mới (vd: báo cáo dị ứng) = tạo class Visitor mới — không cần sửa CoffeeItem, PastryItem, hay ComboItem. Đây là OCP trong thực tế.

Double Dispatch

Visitor dùng double dispatch — hai polymorphic call:

  1. item.Accept(visitor) — element quyết định overload Visit() nào được gọi
  2. visitor.Visit(this) — visitor execute logic đúng cho type này

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

  • Expression tree (ExpressionVisitor) — duyệt và transform LINQ expression tree
  • Roslyn syntax walker (CSharpSyntaxWalker) — visit AST node để phân tích code
  • Serialization — visiting object graph để tạo JSON/XML
  • Compiler pipeline — visiting IR node để tối ưu hóa

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

  • Cần thêm thao tác cho object structure ổn định mà không sửa class
  • Object structure ít thay đổi, nhưng thao tác thay đổi thường xuyên
  • Cần duyệt structure phức tạp (AST, DOM, composite) với hành vi khác nhau

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

  • Object structure thay đổi thường xuyên — thêm element type mới cần update tất cả visitor
  • Chỉ một hoặc hai thao tác — method trên element đơn giản hơn
  • Element type không biết lúc compile time — dùng dynamic dispatch

Điểm chính (Key Takeaways)

  • Visitor thêm thao tác mà không sửa element (OCP cho thao tác)
  • Dùng double dispatch để route đúng method Visit()
  • Tốt nhất khi element structure ổn định nhưng thao tác thay đổi
  • Thêm element type mới là điểm yếu — nó phá tất cả visitor

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

Q: Double dispatch là gì? Single dispatch: method được gọi phụ thuộc runtime type của receiver. Double dispatch: method được gọi phụ thuộc cả receiver và runtime type của argument. Visitor đạt điều này qua Accept() gọi Visit(this)this được resolve lúc compile time thành concrete type.

Q: Tại sao Visitor bị phá khi thêm element type mới? Mỗi visitor phải implement Visit() cho mọi element type. Thêm TeaItem nghĩa là mọi visitor hiện có cần method Visit(TeaItem) mới. Đây là đánh đổi của pattern: dễ thêm thao tác, khó thêm element.

Q: ExpressionVisitor trong LINQ dùng pattern này như thế nào? ExpressionVisitorVisitBinary(), VisitConstant(), VisitMethodCall(), v.v. LINQ provider (EF Core) override các method này để dịch expression C# thành SQL. Expression tree là structure, mỗi provider là visitor.

  • Composite — visitor thường duyệt cấu trúc composite
  • Iterator — duyệt mà không có thao tác
  • Strategy — mục đích khác (hoán đổi thuật toán)
  • SOLID — OCP — Visitor là OCP áp dụng cho thao tác