Visitor Pattern
Represent an operation to be performed on elements of an object structure. Visitor lets you define new operations without changing the classes of the elements on which it operates. The foundation of compilers, AST processors, document exporters, and tax calculators.
Why Visitor Exists
You have a document model — an object structure with different node types: Paragraph, Heading, Image, Table. Now you need to add operations: export to HTML, export to Markdown, word count, spell check, generate table of contents. The naive approach: add each operation as a method on every node class.
What goes wrong:
- Open/Closed violation — adding a new operation (spell check) requires editing every node class: Paragraph, Heading, Image, Table, CodeBlock
- Unrelated logic in domain classes —
Paragraphis a data model class; it shouldn’t know about HTML rendering, Markdown syntax, or spell-checking algorithms - Scattered operation logic — the “HTML export” logic is spread across 5 files instead of living in one cohesive class
- Can’t add operations without source access — if the node classes are in a library you don’t control, you can’t add methods to them
- Single Responsibility violation — each node class has multiple reasons to change: data model changes AND operation changes
This is the problem Visitor solves — separate operations from object structure. Each operation becomes its own class (a “visitor”) with one method per node type. Adding a new operation = adding one class, not editing all node classes.
What Is Visitor?
Visitor is a behavioral pattern that lets you define new operations on an object structure without modifying the classes of the elements. You achieve this with “double dispatch”: each element has an accept(visitor) method that calls back to the visitor’s type-specific method. The operation logic lives in visitor classes; the elements remain unchanged when new operations are added.
— Gamma, Helm, Johnson, Vlissides (1994)
accept(inspector) — “let the inspector in.”
Key insight — double dispatch: In Java, method calls are dispatched on the receiver’s runtime type (single dispatch). But Visitor needs dispatch on two types: the element AND the operation. The trick: the element’s accept() calls visitor.visitParagraph(this) — now the call dispatches on both the visitor type (which operation?) and the element type (which visit* method?). This “double dispatch” simulates multi-methods in a single-dispatch language.
accept(Visitor v) that calls v.visitThisType(this)HtmlExportVisitor. One file, one responsibility. Easy to test in isolation.Participants & Structure
| Participant | Role | In the Analogy |
|---|---|---|
| Visitor (interface) | Declares a visit method for each concrete element type: visitParagraph(Paragraph), visitHeading(Heading), etc. One method per element type. | The “inspector protocol” — an inspector must know how to audit restaurants, shops, and factories. |
| Concrete Visitor | Implements all visit methods for one specific operation. HtmlExportVisitor has HTML logic for every element type. WordCountVisitor has counting logic for every type. | The tax inspector (knows tax rules for all business types). A separate health inspector is another visitor. |
| Element (interface) | Declares accept(Visitor v). Every element in the structure must implement this. | The “businesses must allow inspectors in” contract. |
| Concrete Element | Implements accept() by calling v.visitThisType(this). The element dispatches to the correct visitor method based on its own type. Has its own data fields. | Restaurant, Shop, Factory — each lets the inspector in and says “I’m a restaurant.” |
| Object Structure | A collection or composite that holds elements. Can iterate over elements and call accept() on each. Often uses Iterator internally. | The city’s business registry — tells the inspector which businesses to visit. |
- Visitor is Open/Closed for operations (add new visitors freely) but NOT for element types (adding a new element means editing every visitor).
- This is the opposite tradeoff of plain polymorphism (which is Open/Closed for types but not for operations).
- Use Visitor when the element hierarchy is stable (types rarely change) but operations change frequently.
The Pattern In Motion
Scenario: A Document has elements (Paragraph, Heading). We apply an HtmlExportVisitor to convert all elements to HTML without modifying any element class.
HtmlExportVisitor html = new HtmlExportVisitor()for (DocElement el : doc) el.accept(html)accept().Paragraph.accept(v) calls v.visitParagraph(this)visitParagraph() runs.HtmlExportVisitor.visitParagraph(p) generates <p>text</p>Heading.accept(v) calls v.visitHeading(this)HtmlExportVisitor.visitHeading(h) generates <h2>text</h2>. Different element, different logic, same visitor.instanceof?: - You could write
if (el instanceof Paragraph p) html += .... - But: (1) you must remember every type in every operation; (2) the compiler can’t check exhaustiveness (if you add a type,
instanceofchains silently ignore it); (3) with Visitor, the compiler forces you to implement avisitmethod for every type — exhaustiveness is guaranteed at compile time.
You Already Use This
Visitor is found wherever a stable type hierarchy needs extensible operations — especially in compiler tooling, file systems, and annotation processing.
javax.lang.model.element.ElementVisitor<R,P> — the annotation processing API uses Visitor to process Java source elements. Elements (TypeElement, ExecutableElement, VariableElement) are stable; operations (generate code, validate annotations, extract metadata) are extensible. AbstractElementVisitor6 is the base. java.nio.file.FileVisitor<Path> — walks a file tree. You implement visitFile(), preVisitDirectory(), postVisitDirectory(), visitFileFailed(). The file system structure is stable (files + directories); operations (search, delete, copy, size calculation) are extensible via different visitor implementations. javax.lang.model.type.TypeVisitor<R,P> — visits Java type mirrors during compilation. Type hierarchy (DeclaredType, ArrayType, PrimitiveType, WildcardType) is fixed by the language spec; operations on types change with each annotation processor. ASM library (org.objectweb.asm.ClassVisitor) — the most widely-used bytecode manipulation library. ClassVisitor, MethodVisitor, FieldVisitor traverse JVM class files. Bytecode structure is stable (defined by JVM spec); operations (instrument, transform, analyze) are endlessly extensible. - Modern Java offers an alternative to classic Visitor via
sealed interface+switchwith pattern matching. - The compiler checks exhaustiveness:
switch (element) { case Paragraph p -> ...; case Heading h -> ...; }. - This achieves the same double dispatch without
accept()methods. - Classic Visitor remains valuable for pre-Java 17 code and when accumulating state across visits.
Build It Once
Domain: Document Export System. Elements: Paragraph, Heading, Image. Visitors: HtmlExportVisitor, WordCountVisitor. Adding a new export format = one new visitor class, zero changes to elements.
- Visitors can carry state across visits.
WordCountVisitoraccumulates acountfield as it visits each element.HtmlExportVisitoraccumulates aStringBuilder.- This is a key advantage over
instanceofswitches: the visitor’s state naturally spans the entire traversal.
Common Mistakes
accept(): - The
accept()method should be exactly one line:v.visitThisType(this). - If you add logic, pre-processing, or state changes inside
accept(), you’re defeating the pattern. - The element dispatches; the visitor does the work.
- Fix: Keep
accept()as a pure dispatch mechanism. All operation logic belongs in the visitor’svisit*methods.
- If visitor methods need access to private fields, developers add public getters that expose internal state.
- This weakens encapsulation.
- Fix: Use package-private access, records (fields are implicitly accessible), or pass only what the visitor needs via the method parameter. If a visitor requires deep internal state, reconsider whether the operation belongs on the element itself.
- If your hierarchy has only one element type, Visitor is overkill — just add a method to the class.
- Visitor pays off with 3+ types where operations span the hierarchy.
- Rule: At least 3 element types AND at least 2 operations that span all of them before considering Visitor.
- New code that implements
DocumentVisitormust handle ALL types. - If a visitor doesn’t care about images, it still needs an empty
visitImage(). - Fix: Provide an
AbstractDocumentVisitorbase class with no-op defaults. Concrete visitors override only the methods they care about. This avoids boilerplate and is robust against new types (they fall through to the default).
When To Use Visitor
- The element hierarchy is stable (types rarely change) but you frequently add new operations that span all types
- You want to keep element classes clean — they should hold data, not operation logic (export, serialization, analysis)
- You need compile-time exhaustiveness — the compiler forces every visitor to handle every type (impossible with
instanceofchains) - Operations need to accumulate state across multiple elements (word count, total size, collected errors)
- You’re building a compiler, AST processor, or document model where node types are defined by a spec and operations are user-defined
- The element hierarchy changes frequently — every new type breaks all visitors (use polymorphism or pattern matching instead)
- You have few element types (1-2) with few operations — just put the method on the class
- You’re on Java 17+ with sealed classes —
switchwith pattern matching gives exhaustiveness without the Visitor boilerplate - Operations don’t span the entire hierarchy — if each operation only touches one type, Visitor adds no value
| Approach | Open/Closed for | When to pick it |
|---|---|---|
| Visitor ← this | Operations (not types) | Stable types, many operations, need cross-type state |
| Polymorphism (virtual methods) | Types (not operations) | Stable operations, types change often |
| Pattern matching (Java 17+) | Both (with sealed) | Modern Java, exhaustive switch, less boilerplate |
| instanceof chains | Neither (fragile) | Quick prototypes only — no exhaustiveness guarantee |
Problems To Solve
Visitor problems test whether you can implement double dispatch, add new operations without modifying elements, handle composite structures, and understand the tradeoff of extensibility on the operations axis vs. the types axis.
| Difficulty | Problem | Key Insight |
|---|---|---|
| Easy | Shape Area & Perimeter Shape hierarchy: Circle(radius), Rectangle(w, h), Triangle(a, b, c). Implement two visitors: AreaVisitor (calculates area for each) and PerimeterVisitor (calculates perimeter). Shapes have an accept() method. Adding a JsonExportVisitor later should require zero changes to shape classes. | Tests basic Visitor structure with a clean 3-element hierarchy. Each visitor accumulates results. The key: shapes are pure data containers with accept() only; all calculation logic is in visitors. Demonstrates the Open/Closed benefit clearly. |
| Medium | AST Evaluator + Pretty Printer Expression AST: NumberLiteral(value), BinaryOp(left, op, right), UnaryOp(op, operand). Implement: (1) EvalVisitor — evaluates the expression tree and returns a numeric result. (2) PrettyPrintVisitor — produces a parenthesized string like (3 + (4 * 2)). Handle operator precedence in pretty printing. | Tests Visitor on a recursive composite structure. The evaluator demonstrates accumulating results bottom-up (visit children first, then combine). Pretty printer tests string construction with proper nesting. Both visitors traverse the same tree with completely different logic — proving the separation of operations from structure. |
| Medium | File System Analysis Nodes: File(name, size, extension), Directory(name, children). Implement visitors: (1) SizeCalculator — total bytes. (2) FileCounter — count by extension. (3) SearchVisitor — find files matching a pattern. Directories delegate accept() to all children recursively. | Tests Visitor on a composite (tree) structure. Directories must call accept() on children — the visitor inherently traverses the tree. SizeCalculator accumulates across the entire tree. FileCounter uses a Map to count by extension. This mirrors java.nio.file.FileVisitor in the stdlib. |
| Hard | Type Checker for Mini Language AST nodes: IntLiteral, BoolLiteral, VarRef, BinaryExpr(+, -, ==, &&), IfExpr(cond, then, else), LetExpr(binding, body). Implement TypeCheckVisitor that validates type correctness: + only on ints, && only on bools, if condition must be bool, branches must return the same type. Maintain a type environment (symbol table). Report all errors. | Tests Visitor as used in real compilers. The type checker maintains a type environment (HashMap of variable โ type) as visitor state. visitVarRef() looks up the type; visitLetExpr() extends the environment for the body. visitBinaryExpr() checks operand types match the operator. Error accumulation across the full AST is a key visitor pattern use case. This is the original GoF motivation. |
- When asked about Visitor, the interviewer wants to see: (1) double dispatch:
element.accept(v)โv.visitElement(this); (2) clear understanding of the tradeoff: Open/Closed for operations, NOT for types; (3)FileVisitor<Path>or annotation processing as JDK examples; (4) when NOT to use it (unstable hierarchy, Java 17+ sealed + switch). - Stand-out answers mention: the connection to the Expression Problem (can’t be Open/Closed for BOTH types and operations simultaneously in Java without language extensions), how sealed interfaces + pattern matching offer a modern alternative, and that ASM/bytecode tooling is the most widely deployed Visitor in the Java ecosystem.