Design Patterns · Behavioral Patterns
Visitor
Add operations across a stable set of element types. Use accept to select the type-specific visitor method without a client-side type switch.
This lesson follows Template Method and uses the trees from Composite.
Visitor puts an operation in a separate object with one method per concrete element type. Each element accepts the visitor and selects its matching method. New operations become easier to add. New element types become harder.
Separate operations from stable elements
A document browser has files and folders. It needs size calculation today, a listing tomorrow, and a validation operation next month. Putting every operation inside FileNode and FolderNode makes those classes grow for unrelated reasons.
interface NodeVisitor {
file(node: FileNode): number;
folder(node: FolderNode): number;
}
interface DocumentNode { accept(visitor: NodeVisitor): number }
class FileNode implements DocumentNode {
constructor(readonly bytes: number) {}
accept(visitor: NodeVisitor) { return visitor.file(this); }
}
class FolderNode implements DocumentNode {
constructor(readonly children: readonly DocumentNode[]) {}
accept(visitor: NodeVisitor) { return visitor.folder(this); }
}
class SizeVisitor implements NodeVisitor {
file(node: FileNode) { return node.bytes; }
folder(node: FolderNode): number {
return node.children.reduce((sum, child) => sum + child.accept(this), 0);
}
}
class CountVisitor implements NodeVisitor {
file(_node: FileNode) { return 1; }
folder(node: FolderNode): number {
return node.children.reduce((sum, child) => sum + child.accept(this), 0);
}
}
const root = new FolderNode([new FileNode(100), new FileNode(250)]);
root.accept(new SizeVisitor()); // 350 bytes
root.accept(new CountVisitor()); // 2 files
This example uses a numeric result for both operations. A different visitor contract can use void and accumulate output, or parameterize its result type. Here each visitor owns folder traversal and assumes an acyclic tree. Traversal can instead live in an iterator or a separate walker.
Follow the two selections
Step through
Dispatch a file to SizeVisitor
Select the concrete element
element.accept(sizeVisitor) → FileNode.accept
The first method selection follows the element's runtime type.
Select the type-specific operation
FileNode.accept → visitor.file(this)
FileNode knows it is a file. It chooses the file method explicitly rather than asking the caller to inspect its type.
Run the concrete visitor
SizeVisitor.file(node) → node.bytes
The visitor's runtime implementation supplies the operation. These two selections are the idea behind double dispatch.
Try it
Change the operation without changing the nodes
file 1 → 100 bytes
file 2 → 250 bytes
folder → 350 bytes
file 1 → 1
file 2 → 1
folder → 2 files
Choose which dimension changes often
| Change | Visitor cost |
|---|---|
| Add an operation | add a concrete visitor implementing all existing element methods |
| Add an element type | add an accept implementation, extend the visitor interface, and update every visitor |
Visitor requires an extension point on each element
A completely unmodifiable class hierarchy cannot suddenly accept visitors. Adding accept is an initial change. A subclass that inherits the wrong accept can dispatch as its parent and skip subtype behavior. Each supported concrete element type needs the correct dispatch.
Visitors need access to the data their operation uses. Expose meaningful read operations, not every private field. If the hierarchy changes often and the operations are few, ordinary methods or a discriminated-union switch can be clearer.
Check yourself
FileNode and FolderNode stay stable while new operations arrive. Which direction does Visitor make cheaper?
Correct. Each visitor supplies behavior for the existing type catalog.
Not quite. New types expand the visitor interface and affect each implementation.
Map the roles
| Role | In this example | Job |
|---|---|---|
| Visitor | NodeVisitor | Declares one visiting method per concrete element class. With overloading, the methods can share a name, but their parameter types must differ. |
| Concrete visitors | SizeVisitor, CountVisitor | Implement several versions of the same behavior, one for each concrete element class |
| Element | DocumentNode | Declares an accept method that takes a visitor typed as the visitor interface |
| Concrete elements | FileNode, FolderNode | Implement accept by calling the visitor method that matches their own class. Every subclass must override it, even if a base class already implements it. |
| Client | the code holding root | Usually represents a collection or a complex structure such as a composite tree. Works with elements through the element interface, so it rarely knows their concrete classes. |
Why not skip accept and give the visitor overloaded methods named visit? Because overloads are chosen at compile time from the declared type. A loop over DocumentNode values only knows each item as a DocumentNode, so the compiler cannot pick visit(file: FileNode). Asking the element to call back is what selects the right method at runtime.
// Does not work: overloads are resolved from the static type DocumentNode.
for (const node of nodes) visitor.visit(node);
// Works: each element knows its own class and calls the matching method.
for (const node of nodes) node.accept(visitor);
Check yourself
Overloaded visit(file) and visit(folder) methods look simpler. Why do they not replace accept?
Not quite. Even in Java or C#, which allow overloads, the overload is still chosen from the static type.
Correct. Double dispatch lets the element's runtime class choose the visitor method.
Reach for it when new operations keep arriving for stable classes
- you need to perform an operation on all elements of a complex object structure, such as an object tree. A visitor implements the operation for every element class it must handle.
- you want to clean auxiliary behavior out of your business classes. Extract the non-primary behaviors into visitors, so the main classes focus on their main job.
- a behavior makes sense for only some classes in a hierarchy. Put it in a visitor, implement the visiting methods for the relevant classes, and leave the rest empty.
Check yourself
Folder and file classes should stay focused on storage, but the team keeps adding export, audit, and preview features to them. Which Visitor use is this?
Correct. Each feature becomes a visitor, and the node classes stay small.
Not quite. These features apply to every node type. The pressure is keeping the node classes focused.
Implement it in seven steps
Refactor existing code toward the pattern in this order:
- Declare the visitor interface with one visiting method per concrete element class in the program.
- Declare the element interface. If you already have an element class hierarchy, add an abstract
acceptmethod to its base class that takes a visitor. - Implement
acceptin every concrete element class. It must redirect the call to the visitor method that matches the element's class. - Keep element classes working with visitors only through the visitor interface. Visitors, however, must know every concrete element class, because those classes appear as parameter types in the visiting methods.
- For each behavior that cannot live inside the element hierarchy, create a concrete visitor and implement all its visiting methods.
- If a visitor needs private members of an element, either make those members public, which weakens the element's encapsulation, or nest the visitor class inside the element class, if your language supports nested classes.
- Have the client create visitor objects and pass them into elements through
accept.
Check yourself
A ZipFileNode extends FileNode inherits accept without overriding it. What goes wrong?
Correct. Every concrete element must override accept to select its own visiting method.
Not quite. The inherited accept calls visitor.file(this), the parent's method.
Name what it costs
| You gain | You pay |
|---|---|
| New behavior across several classes without changing them (open/closed) | Every visitor must be updated whenever an element class is added to or removed from the hierarchy |
| Several versions of one behavior live in one class (single responsibility) | Visitors may lack access to the private fields and methods they need |
| A visitor can accumulate information while it walks a structure, such as a running total over a tree | Each element class needs a correct accept method |
Check yourself
You add a LinkNode element class. How many existing classes must change?
Correct. New element types are the expensive direction for Visitor.
Not quite. Every visitor must learn how to handle the new type.
Do not confuse it with its neighbors
| Pattern | How it relates |
|---|---|
| Command | Think of Visitor as a more powerful Command: its objects can run operations over objects of different classes. |
| Composite | Use a visitor to run an operation over an entire composite tree. |
| Iterator | Combine them to traverse a complex structure and run an operation on its elements, even when the elements belong to different classes. |
Check yourself
A tree's traversal order keeps changing, but the per-node operation stays the same. Which pattern should own the traversal?
Not quite. Command packages one request. It does not walk a structure.
Correct. Keeping traversal out of the visitor lets you change the order without rewriting every visitor.
Retrieve and apply
Check yourself
A plugin system adds new node types every week, while it has only one stable operation. Does Visitor make that growth cheap?
Correct. Visitor favors adding operations across stable types. This system changes the expensive dimension.
Not quite. Open/closed applies to a chosen axis of change. Visitor makes operations extensible while coupling visitors to the element catalog.
Name which grows faster in a feature: element types or operations. Continue to Choose a Pattern to retrieve the whole catalog from concrete design pressures.
Source: Alexander Shvets, Dive Into Design Patterns (深入设计模式), Chinese edition v2021-1.25. Visitor, printed pages 369–383. Explanations, examples, and exercises are adapted for this course.