Skip to main content

Visitor Pattern

Definition​

The Visitor represents an operation to be performed on the elements of an object structure. It lets you define new operations without changing the classes of the elements on which it operates.

Coffee Shop Example​

Our menu has different item types: CoffeeItem, PastryItem, and ComboItem. We want to generate different reports β€” a price report, a calorie report, and an ingredient list β€” without adding methods to each item type. Each report is a Visitor.

Structure​

Element Interface​

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

Concrete Elements​

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)
{
// Double dispatch: first the combo accepts, then each child accepts
visitor.Visit(this);
foreach (var item in Items)
item.Accept(visitor);
}
}

Visitor Interface​

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

Concrete Visitors​

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($" {'"TOTAL"',-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($" {'"TOTAL"',-20} {_total} kcal");
}

public class IngredientListVisitor : IMenuVisitor
{
private readonly HashSet<string> _allIngredients = new();

public void Visit(CoffeeItem item)
{
Console.WriteLine($" {item.Name}: {string.Join(", ", item.Ingredients)}");
foreach (var ing in item.Ingredients) _allIngredients.Add(ing);
}

public void Visit(PastryItem item)
{
Console.WriteLine($" {item.Name}: {string.Join(", ", item.Ingredients)}");
foreach (var ing in item.Ingredients) _allIngredients.Add(ing);
}

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

public void PrintUnique() =>
Console.WriteLine($" Unique ingredients ({_allIngredients.Count}): {string.Join(", ", _allIngredients)}");
}

Client​

// Build the menu
var menu = new IMenuElement[]
{
new CoffeeItem("Latte", 4.00m, 190, new() { "Espresso", "Steamed Milk" }),
new CoffeeItem("Espresso", 2.50m, 5, new() { "Espresso" }),
new PastryItem("Croissant", 3.00m, 230, new() { "Flour", "Butter", "Yeast" }),
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", "Sugar" }),
}),
};

// Generate different reports β€” same data, different operations
Console.WriteLine("=== Price Report ===");
var priceVisitor = new PriceReportVisitor();
foreach (var item in menu)
item.Accept(priceVisitor);
priceVisitor.PrintTotal();

Console.WriteLine("\n=== Calorie Report ===");
var calorieVisitor = new CalorieReportVisitor();
foreach (var item in menu)
item.Accept(calorieVisitor);
calorieVisitor.PrintTotal();

Console.WriteLine("\n=== Ingredients Report ===");
var ingredientVisitor = new IngredientListVisitor();
foreach (var item in menu)
item.Accept(ingredientVisitor);
ingredientVisitor.PrintUnique();
tip

Adding a new report (e.g., allergy report) means creating a new Visitor class β€” zero changes to CoffeeItem, PastryItem, or ComboItem. This is the Open/Closed Principle in action.

Double Dispatch​

Visitor uses double dispatch β€” two polymorphic calls:

  1. item.Accept(visitor) β€” the element decides which Visit() overload to call
  2. visitor.Visit(this) β€” the visitor executes the correct logic for this type

Without double dispatch, C# would call the base type overload, not the concrete type.

.NET Real-World Usage​

  • Expression trees (ExpressionVisitor) β€” traverse and transform LINQ expression trees
  • Roslyn syntax walkers (CSharpSyntaxWalker) β€” visit AST nodes for code analysis
  • Serialization β€” visiting object graphs to produce JSON/XML
  • Compiler pipelines β€” visiting IR (intermediate representation) nodes for optimization

When to Use​

  • You need to add operations to a stable object structure without modifying the classes
  • The object structure rarely changes, but operations change frequently
  • You need to traverse a complex structure (AST, DOM, composite) with different behaviors

When NOT to Use​

  • The object structure changes frequently β€” adding a new element type requires updating all visitors
  • Only one or two operations β€” methods on the elements are simpler
  • The element types are not known at compile time β€” use dynamic dispatch

Key Takeaways​

  • Visitor adds operations without modifying elements (OCP for operations)
  • Uses double dispatch to route the correct Visit() method
  • Best when the element structure is stable but operations vary
  • Adding new element types is the pattern's weakness β€” it breaks all visitors

Interview Questions​

Q: What is double dispatch? Single dispatch: the method called depends on the receiver's runtime type. Double dispatch: the method called depends on both the receiver and the argument's runtime type. Visitor achieves this via Accept() calling Visit(this) β€” this is resolved at compile time to the concrete type.

Q: Why does Visitor break when you add a new element type? Every visitor must implement Visit() for every element type. Adding TeaItem means every existing visitor needs a new Visit(TeaItem) method. This is the pattern's trade-off: easy to add operations, hard to add elements.

Q: How does ExpressionVisitor in LINQ use this pattern? ExpressionVisitor has VisitBinary(), VisitConstant(), VisitMethodCall(), etc. LINQ providers (EF Core) override these to translate C# expressions into SQL. The expression tree is the structure, and each provider is a visitor.

  • Composite β€” visitor often traverses composite structures
  • Iterator β€” traversal without operations
  • Strategy β€” different intent (swap algorithms)
  • SOLID β€” OCP β€” Visitor is the OCP applied to operations