Behavioral · Gang of Four
Visitor
Visitor is a behavioral pattern that adds a new operation over a fixed set of types without editing those types each time. Each type accepts the visitor; the visitor implements visit for each type.
Example: a folder tree of files and folders. Pretty-print is a method on both. Then finance wants tax, and ops wants a zip export. Each new walk edits File and Folder. Use Visitor when the types are stable and the operations keep growing.
Types × operations
Freeze the columns, grow the rows
: File / Folder. Growing walks: PrettyPrint, Tax, ZipExport. A new is a new class, not a new method on File. That is also OCP aimed at the nodes.
| Walk \ Node | File | Folder |
|---|---|---|
| Pretty-print | print name | print then children |
| Tax | add 18% of price | sum children |
| Zip export | write bytes | enter directory |
| next levy | new visitor method | new visitor method |
node.accept(visitor) → visitor.visit(this)
The right visit method runs because the node knows its own type and passes this. That is the whole trick. In an interview, one visitor that pretty-prints and one that sums tax is enough.
Read the diagram definition
sequenceDiagram
participant C as Client
participant F as FileNode
participant V as TaxVisitor
C->>F: accept(taxVisitor)
F->>V: visit(this)
V->>V: add 18% of priceFreeze the node types
Typical type sets are File and Folder, or a small expression AST. New types should be rare once Visitor is in place.
Give each node accept
accept(Visitor v) { v.visit(this); }
Add operations as visitors
PrettyPrint, Tax, ZipExport — new classes, not new methods on File.
Composite gives you the tree
Composite treats File and Folder the same for structure. Visitor gives you new operations over that tree. An in-memory file system often wants both.
Interpreter evaluates a grammar
Interpreter is matches() on a filter tree. Visitor is a new pass over a tree you already have. Do not call a TaxVisitor an interpreter.
Java: tax without editing every node
Folder’s visit recurses with accept. File’s visit does the leaf work. A third operation is another class. FileNode does not change.
interface Node {
void accept(Visitor v);
}
class FileNode implements Node {
final String name;
final Money price;
public void accept(Visitor v) { v.visit(this); }
}
class FolderNode implements Node {
final List<Node> children;
public void accept(Visitor v) { v.visit(this); }
}
interface Visitor {
void visit(FileNode file);
void visit(FolderNode folder);
}
class TaxVisitor implements Visitor {
Money total = Money.zero();
public void visit(FileNode file) {
total = total.plus(file.price.times("0.18"));
}
public void visit(FolderNode folder) {
folder.children.forEach(c -> c.accept(this));
}
}
class PrettyPrint implements Visitor {
public void visit(FileNode file) { System.out.println(file.name); }
public void visit(FolderNode folder) {
folder.children.forEach(c -> c.accept(this));
}
}Visitor when the types are the thing that grows
Adding export() and tax() on File and Folder
When the types grow instead
A new node type every week breaks every visitor. Then a method on the node, or a strategy the node holds, is the honest shape. Visitor is a bet: operations grow, types do not. State that trade-off clearly.
Use Visitor
File system, expression AST, document model. Finance keeps inventing levies. Ops keeps inventing exports.
Do not
UI widget zoo, payment providers that appear monthly, vehicle types in a parking lot. Those types are the axis that moves.
Java: expression tree size
Interpreter evaluates. A SizeVisitor is a second pass. NumberExpr and AddExpr stay closed.
interface Expr {
void accept(ExprVisitor v);
}
class NumberExpr implements Expr {
final int value;
public void accept(ExprVisitor v) { v.visit(this); }
}
class AddExpr implements Expr {
final Expr left, right;
public void accept(ExprVisitor v) { v.visit(this); }
}
interface ExprVisitor {
void visit(NumberExpr n);
void visit(AddExpr a);
}
class SizeVisitor implements ExprVisitor {
int size;
public void visit(NumberExpr n) { size++; }
public void visit(AddExpr a) { a.left.accept(this); a.right.accept(this); size++; }
}Do not raid privates
Tax should call price(). If the visitor needs the file’s byte buffer, you either made a public question or you are writing a God visitor. Prefer a method the node already had.
One visitor, one walk
PrettyPrint and Tax are two classes. A Visitor that prints and taxes and zips is the God object you just moved off File.
Nearby in LLD