You’re building a compiler that needs to perform multiple operations on an abstract syntax tree: type checking, code generation, optimization. Adding each operation as a method to every node class would bloat the classes and violate the Open/Closed Principle. The Visitor pattern lets you separate algorithms from the objects they operate on, enabling you to add new operations without modifying existing classes.
Problem
Consider a document structure with different element types:
public abstract class DocumentElement
{
public abstract void Export();
public abstract void Print();
public abstract void Validate();
// Adding new operations requires modifying all classes
}
public class TextElement : DocumentElement
{
public override void Export() { /* Text export */ }
public override void Print() { /* Text print */ }
public override void Validate() { /* Text validation */ }
}
public class ImageElement : DocumentElement
{
public override void Export() { /* Image export */ }
public override void Print() { /* Image print */ }
public override void Validate() { /* Image validation */ }
}
Every time you add a new operation (e.g., Compress()), you must modify all element classes. This violates the Open/Closed Principle and makes classes harder to maintain.
Solution
The Visitor pattern lets you separate algorithms from the objects they operate on. It allows you to add new operations to object structures without modifying the classes.
Definition: The Visitor pattern lets you separate algorithms from the objects on which they operate.
Instead of adding operations to classes, you create visitor classes that implement operations. Objects accept visitors, allowing visitors to perform operations on them.
Pseudocode
Element Interface (IDocumentElement)
↓ has method
Accept(Visitor visitor)
↓ implemented by
Concrete Elements (TextElement, ImageElement)
↓ accepts
Visitor Interface (IVisitor)
↓ implemented by
Concrete Visitors (ExportVisitor, PrintVisitor)
↓ visits
Elements
Examples
Here’s a complete, runnable C# implementation:
using System;
using System.Collections.Generic;
// Visitor interface
public interface IVisitor
{
void VisitText(TextElement element);
void VisitImage(ImageElement element);
void VisitParagraph(ParagraphElement element);
}
// Element interface
public interface IDocumentElement
{
void Accept(IVisitor visitor);
}
// Concrete elements
public class TextElement : IDocumentElement
{
public string Content { get; set; }
public TextElement(string content)
{
Content = content;
}
public void Accept(IVisitor visitor)
{
visitor.VisitText(this);
}
}
public class ImageElement : IDocumentElement
{
public string Source { get; set; }
public ImageElement(string source)
{
Source = source;
}
public void Accept(IVisitor visitor)
{
visitor.VisitImage(this);
}
}
public class ParagraphElement : IDocumentElement
{
public List<IDocumentElement> Children { get; set; } = new List<IDocumentElement>();
public void Accept(IVisitor visitor)
{
visitor.VisitParagraph(this);
foreach (var child in Children)
{
child.Accept(visitor);
}
}
}
// Concrete visitors
public class ExportVisitor : IVisitor
{
public void VisitText(TextElement element)
{
Console.WriteLine($"Exporting text: {element.Content}");
}
public void VisitImage(ImageElement element)
{
Console.WriteLine($"Exporting image: {element.Source}");
}
public void VisitParagraph(ParagraphElement element)
{
Console.WriteLine("Exporting paragraph structure");
}
}
public class PrintVisitor : IVisitor
{
public void VisitText(TextElement element)
{
Console.WriteLine($"Printing text: {element.Content}");
}
public void VisitImage(ImageElement element)
{
Console.WriteLine($"Printing image: {element.Source}");
}
public void VisitParagraph(ParagraphElement element)
{
Console.WriteLine("Printing paragraph");
}
}
public class ValidationVisitor : IVisitor
{
public void VisitText(TextElement element)
{
if (string.IsNullOrEmpty(element.Content))
{
Console.WriteLine("Validation error: Text is empty");
}
else
{
Console.WriteLine("Text validation passed");
}
}
public void VisitImage(ImageElement element)
{
if (string.IsNullOrEmpty(element.Source))
{
Console.WriteLine("Validation error: Image source is empty");
}
else
{
Console.WriteLine("Image validation passed");
}
}
public void VisitParagraph(ParagraphElement element)
{
Console.WriteLine("Paragraph validation passed");
}
}
// Usage
class Program
{
static void Main()
{
var document = new ParagraphElement();
document.Children.Add(new TextElement("Hello World"));
document.Children.Add(new ImageElement("photo.jpg"));
document.Children.Add(new TextElement("End of document"));
Console.WriteLine("Export Visitor:");
var exportVisitor = new ExportVisitor();
document.Accept(exportVisitor);
Console.WriteLine("\nPrint Visitor:");
var printVisitor = new PrintVisitor();
document.Accept(printVisitor);
Console.WriteLine("\nValidation Visitor:");
var validationVisitor = new ValidationVisitor();
document.Accept(validationVisitor);
}
}
Output:
Export Visitor:
Exporting paragraph structure
Exporting text: Hello World
Exporting image: photo.jpg
Exporting text: End of document
Print Visitor:
Printing paragraph
Printing text: Hello World
Printing image: photo.jpg
Printing text: End of document
Validation Visitor:
Paragraph validation passed
Text validation passed
Image validation passed
Text validation passed
Real-World Example: Compiler Operations
// AST Node interface
public interface IASTNode
{
void Accept(IVisitor visitor);
}
// Concrete nodes
public class VariableNode : IASTNode
{
public string Name { get; set; }
public string Type { get; set; }
public void Accept(IVisitor visitor)
{
visitor.VisitVariable(this);
}
}
public class FunctionNode : IASTNode
{
public string Name { get; set; }
public List<IASTNode> Parameters { get; set; } = new List<IASTNode>();
public void Accept(IVisitor visitor)
{
visitor.VisitFunction(this);
}
}
// Visitor interface
public interface IVisitor
{
void VisitVariable(VariableNode node);
void VisitFunction(FunctionNode node);
}
// Type checking visitor
public class TypeCheckVisitor : IVisitor
{
public void VisitVariable(VariableNode node)
{
Console.WriteLine($"Type checking variable: {node.Name} ({node.Type})");
}
public void VisitFunction(FunctionNode node)
{
Console.WriteLine($"Type checking function: {node.Name}");
foreach (var param in node.Parameters)
{
param.Accept(this);
}
}
}
// Code generation visitor
public class CodeGenVisitor : IVisitor
{
public void VisitVariable(VariableNode node)
{
Console.WriteLine($"Generating code for variable: {node.Name}");
}
public void VisitFunction(FunctionNode node)
{
Console.WriteLine($"Generating code for function: {node.Name}");
foreach (var param in node.Parameters)
{
param.Accept(this);
}
}
}
// Usage
var func = new FunctionNode { Name = "add" };
func.Parameters.Add(new VariableNode { Name = "a", Type = "int" });
func.Parameters.Add(new VariableNode { Name = "b", Type = "int" });
var typeChecker = new TypeCheckVisitor();
func.Accept(typeChecker);
var codeGen = new CodeGenVisitor();
func.Accept(codeGen);
Shopping Cart Example
public interface IItem
{
void Accept(IVisitor visitor);
}
public class Book : IItem
{
public decimal Price { get; set; }
public string Isbn { get; set; }
public void Accept(IVisitor visitor)
{
visitor.VisitBook(this);
}
}
public class Electronics : IItem
{
public decimal Price { get; set; }
public string SerialNumber { get; set; }
public void Accept(IVisitor visitor)
{
visitor.VisitElectronics(this);
}
}
public interface IVisitor
{
void VisitBook(Book book);
void VisitElectronics(Electronics electronics);
}
public class PriceCalculator : IVisitor
{
public decimal Total { get; private set; } = 0;
public void VisitBook(Book book)
{
Total += book.Price;
Console.WriteLine($"Book price: ${book.Price}");
}
public void VisitElectronics(Electronics electronics)
{
Total += electronics.Price;
Console.WriteLine($"Electronics price: ${electronics.Price}");
}
}
public class TaxCalculator : IVisitor
{
public decimal Tax { get; private set; } = 0;
public void VisitBook(Book book)
{
Tax += book.Price * 0.05m; // 5% tax on books
Console.WriteLine($"Book tax: ${book.Price * 0.05m}");
}
public void VisitElectronics(Electronics electronics)
{
Tax += electronics.Price * 0.10m; // 10% tax on electronics
Console.WriteLine($"Electronics tax: ${electronics.Price * 0.10m}");
}
}
Applications
Enterprise Use Cases:
- Compiler Design: Type checking, code generation, optimization on AST
- Document Processing: Export, print, validate operations on document elements
- Shopping Carts: Price calculation, tax calculation, discount application
- File Systems: Operations on files and directories (backup, search, indexing)
- UI Frameworks: Rendering, event handling on UI components
- Data Structures: Operations on tree structures (traversal, search, modification)
- Reporting Systems: Different report formats from same data structure
- Code Analysis: Static analysis, metrics calculation on code structures
Benefits:
- Follows Open/Closed Principle—add new operations without modifying classes
- Groups related operations in visitor classes
- Makes it easy to add new operations
- Can accumulate state while traversing structure
- Separates algorithms from object structure
When to Use:
- You need to perform operations on object structures
- Operations need to be added frequently
- Object structure is stable but operations vary
- You want to avoid polluting element classes with operation code
- Operations need to work across class hierarchies
Visitor vs Strategy:
- Visitor: Operations work on object structures, operations are separate from objects
- Strategy: Algorithms are interchangeable, chosen externally
Double Dispatch:
- Visitor pattern uses double dispatch
- Element calls
Accept(visitor), which callsvisitor.VisitElement(this) - This ensures the correct visitor method is called for each element type
Key Insight: The Visitor pattern separates operations from object structures. Instead of adding methods to element classes, you create visitor classes that implement operations. Elements accept visitors, allowing visitors to perform operations on them. This makes it easy to add new operations without modifying existing classes, following the Open/Closed Principle.
