Visitor lets you add new operations to a family of related classes without modifying those classes themselves — useful when the object structure (a set of shape types, a set of AST node types) is stable, but the operations performed on it keep growing (render, compute area, serialize, validate...).
public interface IShapeVisitor
{
void Visit(Circle circle);
void Visit(Square square);
}
public interface IShape
{
void Accept(IShapeVisitor visitor);
}
public class Circle : IShape
{
public double Radius { get; set; }
public void Accept(IShapeVisitor visitor) => visitor.Visit(this); // double-dispatch
}
public class Square : IShape
{
public double Side { get; set; }
public void Accept(IShapeVisitor visitor) => visitor.Visit(this);
}
// A new operation — added without touching Circle or Square at all.
public class AreaVisitor : IShapeVisitor
{
public double TotalArea { get; private set; }
public void Visit(Circle circle) => TotalArea += Math.PI * circle.Radius * circle.Radius;
public void Visit(Square square) => TotalArea += square.Side * square.Side;
}
var shapes = new List<IShape> { new Circle { Radius = 2 }, new Square { Side = 3 } };
var areaVisitor = new AreaVisitor();
foreach (var shape in shapes) shape.Accept(areaVisitor);
Console.WriteLine(areaVisitor.TotalArea);
The Accept(this) call is the key mechanic, called double dispatch: it resolves both the concrete shape type and the concrete visitor's matching Visit overload at runtime, which plain method overloading in C# (resolved at compile time, based on the declared type of a variable) can't do on its own for polymorphic collections like the List<IShape> above.