BehavioralC#verifiedVerified
Visitor Pattern in C#
Lets you add new operations to an object structure without modifying the objects themselves, by separating the algorithm from the object structure it operates on.
How to Implement the Visitor Pattern in C#
1Step 1: Define the visitor interface
public interface IShapeVisitor<out TResult>
{
TResult Visit(Circle circle);
TResult Visit(Rectangle rectangle);
}2Step 2: Elements accept visitors
public interface IShape
{
TResult Accept<TResult>(IShapeVisitor<TResult> visitor);
}
public class Circle(double radius) : IShape
{
public double Radius => radius;
public TResult Accept<TResult>(IShapeVisitor<TResult> visitor) =>
visitor.Visit(this);
}
public class Rectangle(double width, double height) : IShape
{
public double Width => width;
public double Height => height;
public TResult Accept<TResult>(IShapeVisitor<TResult> visitor) =>
visitor.Visit(this);
}3Step 3: Concrete visitor computes area
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;
}
// Usage:
// IShape shape = new Circle(5);
// var area = shape.Accept(new AreaCalculator()); // ~78.54using Microsoft.Extensions.Logging;
// [step] Define AST nodes for an expression tree
public interface IExpressionNode
{
TResult Accept<TResult>(IExpressionVisitor<TResult> visitor);
}
public record NumberLiteral(double Value) : IExpressionNode
{
public TResult Accept<TResult>(IExpressionVisitor<TResult> visitor) =>
visitor.VisitNumber(this);
}
public record BinaryExpression(
IExpressionNode Left, string Operator,
IExpressionNode Right) : IExpressionNode
{
public TResult Accept<TResult>(IExpressionVisitor<TResult> visitor) =>
visitor.VisitBinary(this);
}
public record UnaryExpression(
string Operator, IExpressionNode Operand) : IExpressionNode
{
public TResult Accept<TResult>(IExpressionVisitor<TResult> visitor) =>
visitor.VisitUnary(this);
}
public record FunctionCall(
string Name, IReadOnlyList<IExpressionNode> Args) : IExpressionNode
{
public TResult Accept<TResult>(IExpressionVisitor<TResult> visitor) =>
visitor.VisitFunctionCall(this);
}
// [step] Visitor interface for expression processing
public interface IExpressionVisitor<out TResult>
{
TResult VisitNumber(NumberLiteral node);
TResult VisitBinary(BinaryExpression node);
TResult VisitUnary(UnaryExpression node);
TResult VisitFunctionCall(FunctionCall node);
}
// [step] Evaluator visitor computes the result
public class EvaluatorVisitor : IExpressionVisitor<double>
{
public double VisitNumber(NumberLiteral node) => node.Value;
public double VisitBinary(BinaryExpression node)
{
var left = node.Left.Accept(this);
var right = node.Right.Accept(this);
return node.Operator switch
{
"+" => left + right,
"-" => left - right,
"*" => left * right,
"/" => right != 0 ? left / right
: throw new DivideByZeroException(),
"^" => Math.Pow(left, right),
_ => throw new NotSupportedException(
$"Operator: {node.Operator}")
};
}
public double VisitUnary(UnaryExpression node)
{
var operand = node.Operand.Accept(this);
return node.Operator switch
{
"-" => -operand,
"+" => operand,
_ => throw new NotSupportedException(
$"Unary operator: {node.Operator}")
};
}
public double VisitFunctionCall(FunctionCall node)
{
var args = node.Args.Select(a => a.Accept(this)).ToArray();
return node.Name.ToLower() switch
{
"sqrt" => Math.Sqrt(args[0]),
"abs" => Math.Abs(args[0]),
"max" => args.Max(),
"min" => args.Min(),
_ => throw new NotSupportedException(
$"Function: {node.Name}")
};
}
}
// [step] Pretty-printer visitor produces string representation
public class PrettyPrinterVisitor : IExpressionVisitor<string>
{
public string VisitNumber(NumberLiteral node) =>
node.Value.ToString("G");
public string VisitBinary(BinaryExpression node) =>
$"({node.Left.Accept(this)} {node.Operator} {node.Right.Accept(this)})";
public string VisitUnary(UnaryExpression node) =>
$"({node.Operator}{node.Operand.Accept(this)})";
public string VisitFunctionCall(FunctionCall node)
{
var args = string.Join(", ",
node.Args.Select(a => a.Accept(this)));
return $"{node.Name}({args})";
}
}Visitor Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Visitor Pattern in the Real World
“Think of a tax auditor visiting different types of businesses—a restaurant, a law firm, a retail shop. The auditor (visitor) knows exactly what to examine at each type of business and applies the appropriate inspection procedure. The businesses (elements) simply let the auditor in; they don't change their own operations to accommodate the audit.”