Java 4 min read

The Visitor Design Pattern: Add Operations Without Modifying Classes

Master the Visitor Pattern in Java. Learn how to add new operations to object structures using double dispatch.

MR

Moshiour Rahman

Advertisement

The Problem: Adding Operations to Closed Hierarchies

Imagine you have a document with different elements:

  • Paragraph
  • Image
  • Table

Now you need to add operations like:

  • Export to PDF
  • Export to HTML
  • Calculate word count

Naive approach: Add methods to each class:

class Paragraph {
    void exportPDF() { ... }
    void exportHTML() { ... }
    void wordCount() { ... }
}

Problems:

  1. Violates Open/Closed Principle (modifying existing classes)
  2. Violates Single Responsibility (each class does export + rendering)
  3. Can’t add operations if you don’t own the classes

The Solution: The Visitor Pattern

The Visitor Pattern lets you add operations to objects without modifying their classes. It uses double dispatch to separate algorithms from object structure.

Real-Life Analogy: Tax Inspector 🧑‍💼

A tax inspector visits different building types:

  • House: Checks property tax
  • Shop: Checks business tax + inventory tax
  • Bank: Checks corporate tax + compliance

The inspector (visitor) performs different actions based on the building type (element), but buildings don’t need to know how taxes work.

Visualizing the Pattern

Visitor Pattern

Implementation

1. The Element Interface

// Element: Accepts visitors
public interface DocumentElement {
    void accept(Visitor visitor);
}

2. Concrete Elements

public class Paragraph implements DocumentElement {
    private String text;

    public Paragraph(String text) {
        this.text = text;
    }

    public String getText() { return text; }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this); // Double dispatch!
    }
}

public class Image implements DocumentElement {
    private String url;

    public Image(String url) {
        this.url = url;
    }

    public String getUrl() { return url; }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

3. The Visitor Interface

// Visitor: Defines operations
public interface Visitor {
    void visit(Paragraph paragraph);
    void visit(Image image);
}

4. Concrete Visitors (Operations)

// Export to HTML
public class HTMLExportVisitor implements Visitor {
    private StringBuilder html = new StringBuilder();

    @Override
    public void visit(Paragraph p) {
        html.append("<p>").append(p.getText()).append("</p>\n");
    }

    @Override
    public void visit(Image img) {
        html.append("<img src=\"").append(img.getUrl()).append("\"/>\n");
    }

    public String getHTML() { return html.toString(); }
}

// Word count
public class WordCountVisitor implements Visitor {
    private int wordCount = 0;

    @Override
    public void visit(Paragraph p) {
        wordCount += p.getText().split("\\s+").length;
    }

    @Override
    public void visit(Image img) {
        // Images don't have words
    }

    public int getCount() { return wordCount; }
}

Usage

List<DocumentElement> document = List.of(
    new Paragraph("Hello world"),
    new Image("photo.jpg"),
    new Paragraph("Design patterns are cool")
);

// Export to HTML
HTMLExportVisitor htmlVisitor = new HTMLExportVisitor();
for (DocumentElement elem : document) {
    elem.accept(htmlVisitor);
}
System.out.println(htmlVisitor.getHTML());

// Count words
WordCountVisitor wordCounter = new WordCountVisitor();
for (DocumentElement elem : document) {
    elem.accept(wordCounter);
}
System.out.println("Words: " + wordCounter.getCount()); // 6

Double Dispatch Explained

Single dispatch (normal polymorphism):

elem.render(); // Calls Paragraph.render() or Image.render()

Double dispatch (Visitor):

elem.accept(visitor); // 1st dispatch: calls Paragraph.accept()
// Inside Paragraph.accept():
visitor.visit(this);  // 2nd dispatch: calls visitor.visit(Paragraph)

This allows the operation to depend on both the element type and visitor type.

In The Wild (Real World Examples)

1. Compiler AST Traversal

Abstract Syntax Trees use Visitor to perform operations like:

  • Type checking
  • Code generation
  • Optimization

2. Java NIO File Visitor

Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("Visiting: " + file);
        return FileVisitResult.CONTINUE;
    }
});

Cheat Sheet

FeatureDetails
CategoryBehavioral
Problem SolvedAdding operations without modifying classes
Key implementationaccept(Visitor v) + v.visit(this) (double dispatch)
ProsOpen/Closed (add operations easily), Single Responsibility
ConsBreaking change (adding new element type requires updating all visitors)

Tips to Remember 🧠

  • “Tax Inspector”: Different buildings, one inspector with different procedures.
  • “Double Dispatch”: Element calls visitor, visitor calls back to element.
  • “When to use”: Adding operations is frequent, adding element types is rare.

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.