Learnixo
Back to blog
Backend Systemsintermediate

Visitor — Add Operations Without Modifying Classes

The Visitor pattern in C#: add new operations to a class hierarchy without modifying existing classes. Double dispatch, AST traversal, and when pattern matching replaces Visitor.

Asma Hafeez KhanMay 24, 20264 min read
csharpdesign-patternsvisitorbehavioraldotnetdouble-dispatch
Share:𝕏

Visitor — Add Operations Without Modifying Classes

The Visitor pattern lets you add new operations to a class hierarchy without modifying the classes. It separates algorithms from the object structure they operate on using double dispatch.


Core Implementation

C#
// Visitor interface — one Visit overload per element type
public interface IShapeVisitor<TResult>
{
    TResult Visit(Circle circle);
    TResult Visit(Rectangle rectangle);
    TResult Visit(Triangle triangle);
}

// Base class adds Accept — the only change needed to the hierarchy
public abstract class Shape
{
    public abstract TResult Accept<TResult>(IShapeVisitor<TResult> visitor);
}

public class Circle(double radius) : Shape
{
    public double Radius => radius;
    public override TResult Accept<TResult>(IShapeVisitor<TResult> v) => v.Visit(this);
}

public class Rectangle(double w, double h) : Shape
{
    public double Width => w;
    public double Height => h;
    public override TResult Accept<TResult>(IShapeVisitor<TResult> v) => v.Visit(this);
}

public class Triangle(double b, double h) : Shape
{
    public double Base => b;
    public double Height => h;
    public override TResult Accept<TResult>(IShapeVisitor<TResult> v) => v.Visit(this);
}

// New operation: area — no changes to Shape, Circle, Rectangle, or Triangle
public class AreaCalculator : IShapeVisitor<double>
{
    public double Visit(Circle c)    => Math.PI * c.Radius * c.Radius;
    public double Visit(Rectangle r) => r.Width * r.Height;
    public double Visit(Triangle t)  => 0.5 * t.Base * t.Height;
}

// Another new operation: SVG export — again, zero changes to the hierarchy
public class SvgExporter : IShapeVisitor<string>
{
    public string Visit(Circle c)
        => $"<circle r=\"{c.Radius}\" cx=\"0\" cy=\"0\"/>";
    public string Visit(Rectangle r)
        => $"<rect width=\"{r.Width}\" height=\"{r.Height}\"/>";
    public string Visit(Triangle t)
        => $"<!-- triangle base={t.Base} height={t.Height} -->";
}

// Usage
List<Shape> shapes = [new Circle(5), new Rectangle(4, 6), new Triangle(3, 8)];
var area = new AreaCalculator();
var svg  = new SvgExporter();

foreach (var shape in shapes)
    Console.WriteLine($"Area={shape.Accept(area):F2} SVG={shape.Accept(svg)}");

Invoice Processing Example

C#
public abstract class InvoiceItem
{
    public abstract TResult Accept<TResult>(IInvoiceVisitor<TResult> visitor);
}

public class ProductItem(string name, decimal price, int qty) : InvoiceItem
{
    public string Name => name; public decimal UnitPrice => price; public int Quantity => qty;
    public override TResult Accept<TResult>(IInvoiceVisitor<TResult> v) => v.Visit(this);
}

public class ServiceItem(string desc, decimal rate, double hours) : InvoiceItem
{
    public string Description => desc; public decimal HourlyRate => rate; public double Hours => hours;
    public override TResult Accept<TResult>(IInvoiceVisitor<TResult> v) => v.Visit(this);
}

public interface IInvoiceVisitor<TResult>
{
    TResult Visit(ProductItem item);
    TResult Visit(ServiceItem item);
}

public class TaxCalculator(decimal rate) : IInvoiceVisitor<decimal>
{
    public decimal Visit(ProductItem p) => p.UnitPrice * p.Quantity * rate;
    public decimal Visit(ServiceItem s) => (decimal)(s.HourlyRate * (decimal)s.Hours) * rate;
}

Modern Alternative: Pattern Matching

C#
// Sealed record hierarchies → switch expression replaces Visitor
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public record Triangle(double Base, double Height) : Shape;

static double Area(Shape shape) => shape switch
{
    Circle c    => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    Triangle t  => 0.5 * t.Base * t.Height,
    _           => throw new ArgumentException("Unknown shape"),
};

// Prefer switch expression when:
//   - Hierarchy is sealed / you control it
//   - Operations are simple one-liners
// Use classic Visitor when:
//   - Hierarchy is open or third-party
//   - Operations are complex and benefit from being in separate classes

Interview Answer

"Visitor separates algorithms from the object structure they operate on — add new operations as new Visitor classes without touching the element hierarchy. It relies on double dispatch: shape.Accept(visitor) calls visitor.Visit(this), and since the concrete type of this is known at call time, the correct Visit overload is selected. Classic uses: compiler AST traversal (type checking, code generation, optimisation each as separate visitors), invoice processing, and shape rendering. In modern C# with sealed records, pattern matching switch expressions often replace Visitor for simple cases — the compiler warns on non-exhaustive switches, giving the same safety. Use Visitor when the hierarchy is open (grows over time), third-party (can't add Accept), or when each operation is complex enough to deserve its own class."

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.