Visitor
Behavioral Design Patterns
The Problem: The Bloated Object Hierarchy
Imagine you have a stable set of classes representing the structure of a document. A Document
can contain Paragraph
elements and Image
elements. Now, you need to add several new, unrelated operations that work on these elements:
- Export the document to HTML.
- Export the document to Markdown.
- Extract all the text for word-counting.
The most direct approach is to add a new method (exportToHtml()
, toMarkdown()
, extractText()
) to the Paragraph
and Image
classes.
// BAD: This class gets bloated with unrelated responsibilities.
public class Paragraph {
private String text;
public void exportToHtml() { /* ... */ }
public void toMarkdown() { /* ... */ }
public String extractText() { /* ... */ }
}
public class Image {
private String src;
public void exportToHtml() { /* ... */ }
public void toMarkdown() { /* ... */ }
public String extractText() { /* return image caption */ }
}
This design is flawed. The element classes violate the Single Responsibility Principle; they are now responsible for their own internal logic and for multiple export formats. They also violate the Open/Closed Principle, because every time a new operation is needed (e.g., "export to PDF"), you have to modify every single class in the element hierarchy.
Separating Algorithms from Objects
The Visitor is a behavioral design pattern that lets you separate algorithms from the objects on which they operate. It allows you to define a new operation without changing the classes of the elements on which it operates.
The core idea is to move the operational logic into a separate "visitor" class. You have one visitor class for each distinct operation (HtmlExportVisitor
, MarkdownExportVisitor
). The element objects are "visitable"; their only new responsibility is to have an accept(visitor)
method.
When an operation needs to be performed, the client passes a visitor object to an element's accept
method. The element then calls the correct method on the visitor, passing itself as an argument. This technique, known as double dispatch, ensures that the correct operation is executed for the correct element type.
The Participants
- Visitor: The interface that declares a
visit()
method for each type ofConcrete Element
in the object structure (e.g.,visit(Paragraph p)
,visit(Image i)
). - Concrete Visitor: A class that implements the
Visitor
interface and contains the logic for a specific operation across all element types. - Element (or Visitable): The interface for the objects being visited. It declares an
accept(Visitor v)
method. - Concrete Element: A class that implements the
Element
interface. Itsaccept
method's sole purpose is to call the correctvisit
method on the visitor:visitor.visit(this)
.
Example: A Document Exporter
Let's refactor our document elements to use the Visitor pattern.
// The Element interface
interface IDocumentElement {
void accept(IDocumentVisitor visitor);
}
// Concrete Elements
class Paragraph implements IDocumentElement {
public String text;
public Paragraph(String text) { this.text = text; }
@Override
public void accept(IDocumentVisitor visitor) {
// This is the first dispatch (based on element type)
visitor.visit(this);
}
}
class Image implements IDocumentElement {
public String src;
public Image(String src) { this.src = src; }
@Override
public void accept(IDocumentVisitor visitor) {
visitor.visit(this);
}
}
// The Visitor interface with overloaded visit methods
interface IDocumentVisitor {
// This is the second dispatch (based on visitor and element type)
void visit(Paragraph p);
void visit(Image i);
}
// Concrete Visitor for HTML export
class HtmlExportVisitor implements IDocumentVisitor {
@Override
public void visit(Paragraph p) {
System.out.println("<p>" + p.text + "</p>");
}
@Override
public void visit(Image i) {
System.out.println("<img src='" + i.src + "' />");
}
}
// Concrete Visitor for Text extraction
class TextExtractorVisitor implements IDocumentVisitor {
@Override
public void visit(Paragraph p) {
System.out.println(p.text);
}
@Override
public void visit(Image i) {
// Do nothing for images when extracting plain text
}
}
// The Client
public class Client {
public static void main(String[] args) {
List<IDocumentElement> document = List.of(
new Paragraph("This is the first paragraph."),
new Image("logo.png"),
new Paragraph("This is the second paragraph.")
);
System.out.println("--- Exporting to HTML ---");
IDocumentVisitor htmlVisitor = new HtmlExportVisitor();
for (IDocumentElement element : document) {
element.accept(htmlVisitor);
}
System.out.println("\n--- Extracting Plain Text ---");
IDocumentVisitor textVisitor = new TextExtractorVisitor();
for (IDocumentElement element : document) {
element.accept(textVisitor);
}
}
}
Interview Focus: Analysis and Trade-offs
Benefits:
- Open/Closed Principle: This is the primary benefit. You can add new operations (by creating new visitor classes) without ever changing the element classes.
- Single Responsibility Principle: The logic for a specific operation (e.g., HTML export) is centralized in its visitor class, rather than being scattered across the element classes.
- Gathers Related Operations: All the logic for one operation is cleanly contained in one place.
Drawbacks:
- Violates OCP for Elements (The Critical Trade-off): This is the most important drawback to discuss in an interview. If you need to add a new
Concrete Element
class (e.g., aTable
element), you must update theIDocumentVisitor
interface to add a newvisit(Table t)
method. This is a breaking change that forces you to update every single concrete visitor you've ever written. The pattern makes adding new operations easy but makes adding new element types hard.
How Visitor Relates to Other Concepts
- Double Dispatch: This is the core mechanism that makes the Visitor pattern work. The final method that gets executed depends on two types determined at runtime: the type of the
Visitor
and the type of theElement
. You can explain it as:element.accept(visitor)
dispatches based on the element's type, which in turn callsvisitor.visit(element)
, which dispatches based on the visitor's type. - Composite: The Visitor pattern is the perfect companion for the Composite pattern. You can use a Visitor to perform complex operations on an entire tree structure built with Composite. The visitor can define how to traverse the tree and how to process both leaf and composite nodes.
- Iterator: An Iterator can traverse a collection but is generally type-agnostic. A Visitor, by contrast, leverages the type system to perform different actions for different element types, making it much more powerful for type-sensitive operations.