Learnixo

.NET & C# Development · Lesson 51 of 229

Visitor — Add Operations Without Modifying Classes

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."