Visitor Pattern
Overview
A document of text and image nodes gains rendering, counting and validation operations. Instead of adding another operation to every node class, collect each operation in its own object.
A Visitor provides a method for each element kind. An element accepts the visitor and calls the method for its own type, allowing text and images to receive different treatment.
UML
TextNode and ImageNode implement IDocNode and accept a visitor. TextVisitor and CountVisitor implement IVisitor for rendering and counting respectively.
History
GoF cataloged Visitor as a behavioral pattern in Design Patterns in 1994. It separates operations from the classes that form an object structure.1
Why use it?
A new rendering or counting operation can be another visitor without changing existing element classes. One visitor collects the element-specific parts of that operation.
Adding a new element kind instead changes IVisitor and existing visitors. This favors stable element kinds with frequently added operations.
Example
Render each document element with TextVisitor.
using System;
using System.Collections.Generic;
using System.Linq;
IDocNode[] nodes = { new TextNode("hello"), new ImageNode("cover") };
var visitor = new TextVisitor();
foreach (IDocNode node in nodes) node.Accept(visitor);
foreach (string line in visitor.Lines) Console.WriteLine(line);
interface IDocNode { void Accept(IVisitor visitor); }
interface IVisitor
{
void Visit(TextNode node);
void Visit(ImageNode node);
}
sealed record TextNode(string Value) : IDocNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
sealed record ImageNode(string Name) : IDocNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
sealed class TextVisitor : IVisitor
{
public List<string> Lines { get; } = new();
public void Visit(TextNode node) => Lines.Add(node.Value);
public void Visit(ImageNode node) => Lines.Add("[image: " + node.Name + "]");
}
sealed class CountVisitor : IVisitor
{
public int Texts { get; private set; }
public int Images { get; private set; }
public void Visit(TextNode node) => Texts++;
public void Visit(ImageNode node) => Images++;
}
hello
[image: cover]node.Accept() dispatches to the concrete element; its Visit(this) then selects the visitor operation for that element type. That is why text and image nodes produce different output.
An extended example
Keep the element classes unchanged and count text and image nodes with CountVisitor.
using System;
using System.Collections.Generic;
using System.Linq;
IDocNode[] nodes = { new TextNode("hello"), new ImageNode("cover"), new TextNode("world") };
var visitor = new CountVisitor();
foreach (IDocNode node in nodes) node.Accept(visitor);
Console.WriteLine("text nodes: " + visitor.Texts);
Console.WriteLine("image nodes: " + visitor.Images);
interface IDocNode { void Accept(IVisitor visitor); }
interface IVisitor
{
void Visit(TextNode node);
void Visit(ImageNode node);
}
sealed record TextNode(string Value) : IDocNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
sealed record ImageNode(string Name) : IDocNode
{
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
sealed class TextVisitor : IVisitor
{
public List<string> Lines { get; } = new();
public void Visit(TextNode node) => Lines.Add(node.Value);
public void Visit(ImageNode node) => Lines.Add("[image: " + node.Name + "]");
}
sealed class CountVisitor : IVisitor
{
public int Texts { get; private set; }
public int Images { get; private set; }
public void Visit(TextNode node) => Texts++;
public void Visit(ImageNode node) => Images++;
}
text nodes: 2
image nodes: 1The caller foreach owns traversal, while the visitor owns the operation on each element. On a Composite tree, decide who traverses children to avoid visiting them twice.
Related reading
Each example is a separate .NET 10 console program.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Behavioral patterns, Visitor. ↩