.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
// 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
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
// 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 classesInterview 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)callsvisitor.Visit(this), and since the concrete type ofthisis known at call time, the correctVisitoverload 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 addAccept), or when each operation is complex enough to deserve its own class."