Visitor Pattern
Add operations as visitor objects when the kinds of elements in a structure are relatively stable.
· Last updated: 2026-10-25 · 6 min read
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); }
// Collect the element-specific parts of one operation in its visitor.
interface IVisitor
{
void Visit(TextNode node);
void Visit(ImageNode node);
}
// The node selects its matching visitor method without implementing the operation itself.
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]The caller holds node as IDocNode, so Visit(node) cannot select either the TextNode or ImageNode overload. Each concrete Accept() passes its own typed this to Visit(), selecting the matching operation without type checks in the caller.
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); }
// Collect the element-specific parts of one operation in its visitor.
interface IVisitor
{
void Visit(TextNode node);
void Visit(ImageNode node);
}
// The node selects its matching visitor method without implementing the operation itself.
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.
TypeScript example
Nodes connect their concrete kind to the visitor; visitors own operations such as rendering and counting. A new operation can then be added without another method in every node class.
interface IDocNode { accept(visitor: IVisitor): void; }
// Collect the element-specific parts of one operation in its visitor.
interface IVisitor {
visitText(node: TextNode): void;
visitImage(node: ImageNode): void;
}
// The node selects its matching visitor method without implementing the operation itself.
class TextNode implements IDocNode {
constructor(readonly text: string) {}
accept(visitor: IVisitor): void { visitor.visitText(this); }
}
class ImageNode implements IDocNode {
constructor(readonly caption: string) {}
accept(visitor: IVisitor): void { visitor.visitImage(this); }
}
class TextVisitor implements IVisitor {
readonly lines: string[] = [];
visitText(node: TextNode): void { this.lines.push(node.text); }
visitImage(node: ImageNode): void { this.lines.push("[image: " + node.caption + "]"); }
}
class CountVisitor implements IVisitor {
texts = 0;
images = 0;
visitText(node: TextNode): void { this.texts++; }
visitImage(node: ImageNode): void { this.images++; }
}
const nodes: IDocNode[] = [new TextNode("hello"), new ImageNode("cover"), new TextNode("world")];
const render = new TextVisitor();
const count = new CountVisitor();
for (const node of nodes) { node.accept(render); node.accept(count); }
for (const line of render.lines) console.log(line);
console.log("text nodes: " + count.texts);
console.log("image nodes: " + count.images);hello
[image: cover]
world
text nodes: 2
image nodes: 1TypeScript overload signatures share a single implementation. This version gives the C# Visit(TextNode) and Visit(ImageNode) operations distinct names, visitText and visitImage, called by the matching accept().
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Behavioral patterns, Visitor. ↩