The Visitor is a pattern that adds operations to a stable type structure using visitor objects. It favors adding new operations, at the cost of making it more expensive to incorporate new element types.
Imagine a document with nodes like paragraph, image, and table. The system works well, but every week you’re asked for a new operation:
- “Now I want to export the document to HTML.” -> You go into all classes (
Paragraph,Image…) and add theExportHTML()method. - “Now I want to export it to PDF.” -> You go back into all of them and add
ExportPDF(). - “Now I want to count how many words it has.” -> Again, modifying all classes.
The classes start to accumulate export, analysis, and presentation operations. If that logic changes for reasons unrelated to the model itself, it may make sense to separate it.
Visitor moves each operation to an external class and uses double dispatch to select the appropriate method for each node type.
The Problem: Double Dispatch
You might think: “Okay, I’ll create an Exporter class with an Export(Node node) method, and that’s it.”
The problem is that in languages like C# or Java, if you have a List<Node>, you can’t easily do this:
foreach (Node n in nodes)
{
exporter.Export(n); // ❌ Error: Which overload do I call?
}The compiler only knows that n is a Node; it doesn’t know at compile time if it’s a Paragraph or an Image, so it doesn’t know whether to call Export(Paragraph p) or Export(Image i).
The Solution
The Visitor pattern solves this using a technique called Double Dispatch.
The visitor does not call the element.
The element “accepts” the visitor and calls itself.
:::
Implementation in C#
Let’s implement our document system and exporters.
The Interfaces (The Contract)
First, we define what a visitor can do and what elements must do.
// The Visitor interface: Must know how to handle EACH concrete type
public interface IVisitor
{
void Visit(Paragraph paragraph);
void Visit(Image image);
void Visit(Table table);
}
// The Element interface: Must accept visitors
public interface IDocumentNode
{
void Accept(IVisitor visitor);
}The Concrete Elements (Stable Structure)
Notice the Accept method. It’s the key. By doing visitor.Visit(this), this is no longer an IDocumentNode, it’s a Paragraph (or Image). The compiler now knows exactly which method to call.
public class Paragraph : IDocumentNode
{
public string Text { get; set; }
public Paragraph(string text) => Text = text;
public void Accept(IVisitor visitor)
{
// Key point: Double Dispatch
visitor.Visit(this);
}
}
public class Image : IDocumentNode
{
public string Path { get; set; }
public Image(string path) => Path = path;
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
public class Table : IDocumentNode
{
public int Rows { get; set; }
public Table(int rows) => Rows = rows;
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}The Visitors (The Algorithms)
Now we can create as many algorithms as we want without touching the classes above.
Visitor 1: Export to HTML
public class HtmlExportVisitor : IVisitor
{
public void Visit(Paragraph paragraph)
{
Console.WriteLine($"<p>{paragraph.Text}</p>");
}
public void Visit(Image image)
{
Console.WriteLine($"<img src='{image.Path}' />");
}
public void Visit(Table table)
{
Console.WriteLine($"<table>... ({table.Rows} rows) ...</table>");
}
}Visitor 2: Statistics (Count things)
public class StatisticsVisitor : IVisitor
{
public int WordCount { get; private set; }
public int ImageCount { get; private set; }
public void Visit(Paragraph paragraph)
{
WordCount += paragraph.Text.Split(' ').Length;
}
public void Visit(Image image)
{
ImageCount++;
}
public void Visit(Table table)
{
// Tables don't add words in this example
}
}The Client
class Program
{
static void Main(string[] args)
{
// 1. Create the document structure (can be a list, a tree...)
var document = new List<IDocumentNode>
{
new Paragraph("Hello World"),
new Table(5),
new Image("/img/photo.jpg"),
new Paragraph("End of document")
};
// 2. We want to export to HTML
Console.WriteLine("--- Exporting to HTML ---");
var htmlExporter = new HtmlExportVisitor();
foreach (var node in document)
{
// The second dispatch happens here
node.Accept(htmlExporter);
}
// 3. We want to calculate statistics
Console.WriteLine("\n--- Calculating Statistics ---");
var stats = new StatisticsVisitor();
foreach (var node in document)
{
node.Accept(stats);
}
Console.WriteLine($"Total Words: {stats.WordCount}");
Console.WriteLine($"Total Images: {stats.ImageCount}");
}
}Output:
--- Exporting to HTML ---
<p>Hello World</p>
<table>... (5 rows) ...</table>
<img src='/img/photo.jpg' />
<p>End of document</p>
--- Calculating Statistics ---
Total Words: 5
Total Images: 1Advantages and Disadvantages
| Advantages | Disadvantages |
|---|---|
Open/Closed: You can add new operations (ExportXML, Compress) without modifying the node classes. | Rigid Hierarchy: If you add a new node type (Video), you must modify the IVisitor interface and all existing visitors. |
| SRP: Groups related logic into a single class (all HTML code is together). | Limited Access: The visitor can only access the public members of the elements (it somewhat breaks encapsulation if you need to access private ones). |
The Visitor pattern is very useful when you have an object structure that changes infrequently (e.g., the syntax of a language, an XML document, a 3D scene graph) but you want to frequently add operations to it.