Behavioral

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.

Overview ยท Behavioral ยท Singleton ยท Factory Method ยท Abstract Factory ยท Builder ยท Prototype ยท Adapter ยท Bridge ยท Composite ยท Decorator ยท Facade ยท Flyweight ยท Proxy ยท Observer ยท Strategy ยท Command ยท Template Method ยท Chain of Resp. ยท State ยท Mediator ยท Iterator ยท Visitor ยท Memento ยท Interpreter
Category: Behavioral Difficulty: Advanced Interview: Tier 3 Confused with: Iterator
01
Section One ยท The Problem

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.

Naive approach — operations stuffed into element classes
// โœ— Every new operation = edit EVERY node class class Paragraph { String text; String toHtml() { return "<p>" + text + "</p>"; } String toMarkdown() { return text + "\n"; } int wordCount() { return text.split(" ").length; } // spellCheck()? generateTOC()? Keep adding methods... } class Heading { String text; int level; String toHtml() { return "<h" + level + ">" + text + "</h" + level + ">"; } String toMarkdown() { return "#".repeat(level) + " " + text; } int wordCount() { return text.split(" ").length; } // Same operations, AGAIN, for every node type } // Image, Table, CodeBlock... ALL need these methods. 5 types ร— 5 ops = 25 methods scattered across 5 files.

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 classesParagraph is 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
Without Visitor — operations embedded in every element class
Paragraph toHtml() toMarkdown() wordCount() Heading toHtml() toMarkdown() wordCount() Image toHtml() toMarkdown() wordCount() ✗ Adding spellCheck() โ†’ edit ALL classes ✗ HTML logic scattered across 5 files ✗ Domain classes know about rendering ✗ Can't add ops to library classes ✗ 5 types × 5 ops = 25 scattered methods + new operation? โ†’ must edit Paragraph, Heading, Image, Table, CodeBlock

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.

02
Section Two ยท The Pattern

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.

GoF Intent: “Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.”
— Gamma, Helm, Johnson, Vlissides (1994)
Analogy — a tax inspector visiting businesses: A city has many types of businesses: restaurants, shops, factories. Each business type has different tax rules. Instead of each business calculating its own taxes (embedding tax logic in every business class), the city sends a tax inspector (visitor) who visits each business. The inspector knows how to calculate tax for restaurants, shops, and factories differently. When tax law changes, you train a new inspector — you don’t retrofit every business. When a new business type opens, you add one new method to the inspector. The businesses just need one standard method: 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.

Each element class has one method: accept(Visitor v) that calls v.visitThisType(this)
Double dispatch: the call resolves both which visitor (operation) and which element type. Elements never change when you add operations.
Each operation is a separate Visitor class with one method per element type
All logic for “export to HTML” lives in HtmlExportVisitor. One file, one responsibility. Easy to test in isolation.
Adding a new operation = adding one visitor class
Open/Closed for operations: existing elements and existing visitors are untouched. No shotgun surgery.
The tradeoff: adding a new element type requires updating ALL visitors
Visitor is Open/Closed for operations but NOT for element types. Use it when the element hierarchy is stable but operations change frequently.
03
Section Three ยท Anatomy

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 — UML Class Diagram
«interface» DocumentVisitor + visitParagraph(Paragraph) + visitHeading(Heading) + visitImage(Image) «interface» DocElement + accept(DocumentVisitor) double dispatch HtmlExportVisitor + visitParagraph(p) + visitHeading(h) WordCountVisitor + visitParagraph(p) + visitHeading(h) Paragraph + accept(v) v.visitParagraph(this) Heading + accept(v) v.visitHeading(this) Document (Object Structure) contains elements ■ Blue = interface ■ Green = concrete - - - gold = double dispatch accept(v) → v.visitX(this) = double dispatch New operation? Add a Visitor class. Existing elements untouched. New element type? Update ALL visitors.
The Open/Closed tradeoff:
  • 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.
04
Section Four ยท How It Works

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.

Step 1 — Client creates a visitor: HtmlExportVisitor html = new HtmlExportVisitor()
The visitor encapsulates the entire “export to HTML” operation. It has one method per element type.
Step 2 — Client iterates: for (DocElement el : doc) el.accept(html)
Each element receives the visitor. The client doesn’t know or care about element types — it just calls accept().
Step 3Paragraph.accept(v) calls v.visitParagraph(this)
Double dispatch: the runtime resolves both (1) which visitor (HtmlExport) and (2) which element type (Paragraph). The correct visitParagraph() runs.
Step 4HtmlExportVisitor.visitParagraph(p) generates <p>text</p>
The visitor method has full access to the Paragraph’s data. It builds the HTML output. The Paragraph class has zero HTML knowledge.
Step 5 — Next element: Heading.accept(v) calls v.visitHeading(this)
HtmlExportVisitor.visitHeading(h) generates <h2>text</h2>. Different element, different logic, same visitor.
Visitor — Double Dispatch Sequence
Client Paragraph HtmlExportVisitor accept(visitor) 1st dispatch: element type v.visitParagraph(this) 2nd dispatch: visitor type → generates <p>text</p> Double dispatch = element.accept(v) dispatches on element type + v.visitX(this) dispatches on visitor type Result: the correct operation runs on the correct element without any instanceof / switch
The pattern in pseudocode
// โ”€โ”€ Build a document with mixed elements โ”€โ”€ Document doc = new Document(); doc.add(new Heading("Introduction", 1)); doc.add(new Paragraph("This is the first paragraph.")); doc.add(new Heading("Details", 2)); doc.add(new Paragraph("More content here.")); // โ”€โ”€ Apply HTML export visitor โ”€โ”€ HtmlExportVisitor html = new HtmlExportVisitor(); doc.accept(html); System.out.println(html.getResult()); // <h1>Introduction</h1><p>This is the first paragraph.</p>... // โ”€โ”€ Apply word count visitor (same elements, different operation) โ”€โ”€ WordCountVisitor wc = new WordCountVisitor(); doc.accept(wc); System.out.println("Words: " + wc.getCount()); // Words: 9
Why not just use 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, instanceof chains silently ignore it); (3) with Visitor, the compiler forces you to implement a visit method for every type — exhaustiveness is guaranteed at compile time.
05
Section Five ยท Java Stdlib

You Already Use This

Visitor is found wherever a stable type hierarchy needs extensible operations — especially in compiler tooling, file systems, and annotation processing.

IN JAVA
Example 1 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.
Example 2 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.
Example 3 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.
Example 4 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.
Stdlib usage — FileVisitor walking a directory tree
// FileVisitor โ€” visit files and directories without knowing the tree structure Files.walkFileTree(Path.of("/src"), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { if (file.toString().endsWith(".java")) { System.out.println("Found: " + file); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { System.out.println("Entering: " + dir); return FileVisitResult.CONTINUE; } });
Java 17+ sealed classes + pattern matching:
  • Modern Java offers an alternative to classic Visitor via sealed interface + switch with 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.
06
Section Six ยท Implementation

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.

Java — Visitor Pattern Document System (core)
// โ”€โ”€ Visitor interface (one method per element type) โ”€โ”€ interface DocumentVisitor { void visitParagraph(Paragraph p); void visitHeading(Heading h); void visitImage(Image img); } // โ”€โ”€ Element interface (just accept) โ”€โ”€ interface DocElement { void accept(DocumentVisitor v); } // โ”€โ”€ Concrete Element: each accept() does double dispatch โ”€โ”€ record Paragraph(String text) implements DocElement { public void accept(DocumentVisitor v) { v.visitParagraph(this); } } record Heading(String text, int level) implements DocElement { public void accept(DocumentVisitor v) { v.visitHeading(this); } }
Accumulating state:
  • Visitors can carry state across visits.
  • WordCountVisitor accumulates a count field as it visits each element.
  • HtmlExportVisitor accumulates a StringBuilder.
  • This is a key advantage over instanceof switches: the visitor’s state naturally spans the entire traversal.
07
Section Seven ยท Watch Out

Common Mistakes

Mistake #1 — Using Visitor when element types change frequently: If you keep adding new element types (Table, Video, CodeBlock, Footnote), every addition forces you to edit ALL existing visitors. This is the Visitor tradeoff — it’s Open/Closed for operations but closed for types. Fix: Use Visitor only when the element hierarchy is stable. If types change often, use plain polymorphism (each element implements the operation) or pattern matching (Java 17+ sealed + switch).
✗ Wrong — unstable element hierarchy
// โœ— Adding Table means editing HtmlVisitor, MarkdownVisitor, // WordCountVisitor, SpellCheckVisitor... all existing visitors break interface DocumentVisitor { void visitParagraph(Paragraph p); void visitHeading(Heading h); void visitImage(Image i); void visitTable(Table t); // โ† added this week void visitVideo(Video v); // โ† added this week void visitFootnote(Footnote f); // โ† added this week // Every visitor implementation must handle all of these! }
Mistake #2 — Putting logic in 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’s visit* methods.
Mistake #3 — Breaking encapsulation to support the visitor:
  • 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.
Mistake #4 — Visitor with a single element type:
  • 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.
Mistake #5 — Forgetting the default/fallback visitor:
  • New code that implements DocumentVisitor must handle ALL types.
  • If a visitor doesn’t care about images, it still needs an empty visitImage().
  • Fix: Provide an AbstractDocumentVisitor base 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).
08
Section Eight ยท Decision Guide

When To Use Visitor

Use Visitor When
  • 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 instanceof chains)
  • 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
Avoid Visitor When
  • 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 — switch with 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
Visitor vs. Alternatives
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
Decision Flowchart
Need operations across a type hierarchy? No Not Visitor Yes Element types stable? (rarely add new types) No Polymorphism (virtual methods) Yes Java 17+ with sealed classes available? Yes Switch + sealed (modern alternative) No Visitor Pattern
09
Section Nine ยท Practice

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.
Interview Tip:
  • 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.