Visitor
Allows adding new operations to object structures without modifying their classes.
Context
You have a stable AST of nodes (literal, add, multiply) and you want to add operations (evaluate, print, optimize) without touching the nodes.
Problem
Each new operation forces you to modify all classes in the AST.
Solution
Each node exposes Accept(visitor) and the visitor implements a method for each node type.
#Example in C#
public interface IExprVisitor<T>
{
T VisitLiteral(Literal n);
T VisitAdd(Add n);
}
public abstract record Expr { public abstract T Accept<T>(IExprVisitor<T> v); }
public record Literal(int Value) : Expr { public override T Accept<T>(IExprVisitor<T> v) => v.VisitLiteral(this); }
public record Add(Expr L, Expr R) : Expr { public override T Accept<T>(IExprVisitor<T> v) => v.VisitAdd(this); }
public class Evaluator : IExprVisitor<int>
{
public int VisitLiteral(Literal n) => n.Value;
public int VisitAdd(Add n) => n.L.Accept(this) + n.R.Accept(this);
}In modern C#, pattern matching over
records is often simpler than a full Visitor.
When NOT to use it
- When the hierarchy changes frequently (each change breaks ALL visitors).
- When a
switch expressionwith pattern matching already covers the operation.
Tradeoffs
| Pro | Con |
|---|---|
| Add operations without touching nodes | Adding nodes breaks all visitors |
#behavioral #gof #double-dispatch