Visitor Design Pattern | UML + Code | Low-Level Design (LLD)
The Visitor Design Pattern is a behavioral design pattern that allows you to add new operations to existing object structures without modifying the classes of the objects on which they operate. It cleanly separates algorithms from the object hierarchy they traverse, solving violations of the Open/Closed Principle (OCP) when operations change more frequently than the object types themselves.
1. Why Do We Need the Visitor Pattern? (The Problem Statement)
Imagine building an application that handles multiple document formats—such as TextFile, Image, and Video. These classes inherit from a common parent or implement a common interface.
As the application evolves, new functional requirements arrive:
- Requirement 1: Calculate the file size.
- Requirement 2: Compress the document.
- Requirement 3: Scan the document for viruses.
The Naive Approach: Putting Operations Inside Document Classes
If you add methods (calculateSize(), compress(), scanForVirus()) directly inside the parent class and override them in all child classes, several problems emerge:
- Violation of the Open/Closed Principle (OCP): Every time a new operation is required, you must open, modify, and re-compile existing classes.
- Violation of Single Responsibility Principle (SRP): The classes end up handling core document data plus multiple unrelated business operations (compression, security scanning, size calculation).
- Code Bloat & Testing Complexity: Classes become massive, complex, and difficult to unit test effectively.
2. Core Mental Model & The Real-World Analogy
Real-Life Analogy: The Museum Tour Guide
Imagine walking through a museum filled with different types of exhibits: Paintings, Sculptures, and Historical Manuscripts.
Instead of giving every single exhibit the internal ability to perform every possible action (like "Audit for Insurance", "Clean Artifact", "Restore Color"), you hire a Museum Inspector (Visitor).
The Inspector walks up to each exhibit and says: "Let me inspect you." The exhibit accepts the visitor and says: "Process me according to your specific inspection checklist."
If you want a new checklist (e.g., a "Fire Safety Checklist"), you don't rebuild or modify the paintings or sculptures. You simply create a new Inspector with a new checklist and send them through the museum.
3. The Core Design Assumption: Stable Elements vs. Evolving Operations
Before applying the Visitor Pattern, you must establish a crucial design assumption about the domain:
- Stable Object Hierarchy (Less Likely to Change): We assume that the core document types (
TextFile,Image,Video) are fixed. We are not frequently introducing entirely new types of documents (e.g., we aren't constantly adding PDFs, Spreadsheets, 3D models, etc.). - Unstable Operations (Frequently Changing): Operations (
calculateSize,compress,scanForVirus, and future requirements) are added and modified frequently.
By applying the golden rule of design patterns—"Encapsulate what varies"—we extract the changing operations out of the document classes and put them into a separate Visitor Hierarchy.
4. Step-by-Step Architecture of the Visitor Pattern
To decouple operations from our document classes, we implement two distinct hierarchies: the Element Hierarchy and the Visitor Hierarchy.
A. The Visitor Hierarchy (IFileVisitor)
We define an interface with overloaded visit methods—one for each concrete element type.
B. The Element Hierarchy (FileSysItem)
Every document element exposes a single, polymorphic method called accept(IFileVisitor visitor).
5. Complete Code Evolution (Java Implementation)
Below is the production-grade implementation reflecting the decoupled architecture.
Step 1: Define the Visitor Interface
public interface FileSysVisitor {
void visit(TextFile textFile);
void visit(ImageFile imageFile);
void visit(VideoFile videoFile);
}
Step 2: Define the Element Interface and Base Class
public abstract class FileSysItem {
protected String name;
public FileSysItem(String name) {
this.name = name;
}
public String getName() {
return name;
}
// The single hook method required for the Visitor Pattern
public abstract void accept(FileSysVisitor visitor);
}
Step 3: Implement Concrete Elements
public class TextFile extends FileSysItem {
private String content;
public TextFile(String name, String content) {
super(name);
this.content = content;
}
public String getContent() {
return content;
}
@Override
public void accept(FileSysVisitor visitor) {
visitor.visit(this); // Passes 'this' (TextFile reference)
}
}
public class ImageFile extends FileSysItem {
private byte[] rawData;
public ImageFile(String name, byte[] rawData) {
super(name);
this.rawData = rawData;
}
@Override
public void accept(FileSysVisitor visitor) {
visitor.visit(this); // Passes 'this' (ImageFile reference)
}
}
public class VideoFile extends FileSysItem {
private int durationSeconds;
public VideoFile(String name, int durationSeconds) {
super(name);
this.durationSeconds = durationSeconds;
}
@Override
public void accept(FileSysVisitor visitor) {
visitor.visit(this); // Passes 'this' (VideoFile reference)
}
}
Step 4: Implement Concrete Visitors (Extracting Operations)
public class SizeCalculationVisitor implements FileSysVisitor {
@Override
public void visit(TextFile textFile) {
System.out.println("Calculating size of TextFile: " + textFile.getName());
// Specific size calculation logic for text files
}
@Override
public void visit(ImageFile imageFile) {
System.out.println("Calculating size of ImageFile: " + imageFile.getName());
// Specific size calculation logic for images
}
@Override
public void visit(VideoFile videoFile) {
System.out.println("Calculating size of VideoFile: " + videoFile.getName());
// Specific size calculation logic for videos
}
}
public class CompressionVisitor implements FileSysVisitor {
@Override
public void visit(TextFile textFile) {
System.out.println("Compressing TextFile: " + textFile.getName());
}
@Override
public void visit(ImageFile imageFile) {
System.out.println("Compressing ImageFile (JPEG/PNG): " + imageFile.getName());
}
@Override
public void visit(VideoFile videoFile) {
System.out.println("Compressing VideoFile (MP4/H.264): " + videoFile.getName());
}
}
public class VirusScanVisitor implements FileSysVisitor {
@Override
public void visit(TextFile textFile) {
System.out.println("Scanning TextFile for macros/scripts: " + textFile.getName());
}
@Override
public void visit(ImageFile imageFile) {
System.out.println("Scanning ImageFile for exploits: " + imageFile.getName());
}
@Override
public void visit(VideoFile videoFile) {
System.out.println("Scanning VideoFile container: " + videoFile.getName());
}
}
Step 5: Client Execution
public class Client {
public static void main(String[] args) {
FileSysItem image1 = new ImageFile("vacation.jpg", new byte[1024]);
FileSysItem video1 = new VideoFile("lecture.mp4", 3600);
// Apply Size Calculation Visitor
FileSysVisitor sizeVisitor = new SizeCalculationVisitor();
image1.accept(sizeVisitor);
// Apply Compression Visitor
FileSysVisitor compressionVisitor = new CompressionVisitor();
image1.accept(compressionVisitor);
video1.accept(compressionVisitor);
// Apply Virus Scan Visitor
FileSysVisitor virusVisitor = new VirusScanVisitor();
image1.accept(virusVisitor);
}
}
6. Standard UML Class Diagram Structure
7. Understanding Double Dispatch
A defining characteristic of the Visitor Design Pattern is Double Dispatch.
Conceptually: Single dispatch relies on a single runtime object's type to determine which polymorphic method to execute. Double dispatch uses two distinct runtime type lookups working together to resolve the exact method call.
How Double Dispatch Works in Our Code:
- First Dispatch (Reference 1): In
image1.accept(compressionVisitor);, the runtime evaluates the dynamic type ofimage1(ImageFile), which dispatches execution toImageFile.accept(). - Second Dispatch (Reference 2): Inside
ImageFile.accept(), it executesvisitor.visit(this). Here, the dynamic type ofvisitor(CompressionVisitor) combined withthis(ImageFile) determines the exact overloaded method invoked (CompressionVisitor.visit(ImageFile)).
8. Strategy Pattern vs. Visitor Pattern: Key Differences
Developers often confuse the Strategy Pattern with the Visitor Pattern because both extract behavior into separate classes. However, they solve fundamentally different problems:
Comparative Decision Matrix
- Use Strategy when you have a fixed set of operations, but the algorithms behind those operations vary frequently.
- Use Visitor when the algorithms/operations change constantly and grow over time, but the underlying object data structures remain stable.
9. Real-World Use Cases & Industry Applications
- Game Rendering Engines: Managing multi-pass rendering, collision detection, and physics calculations across diverse game object hierarchies without altering core entity classes.
- Compilers & Abstract Syntax Trees (AST): Traversing syntax trees to perform semantic analysis, type-checking, optimization passes, and code generation.
- Report Generation / Export Tools: Adding new export formats (JSON, XML, PDF, CSV) as separate visitors without touching domain model classes.
10. Summary and Key Takeaways
- Open/Closed Principle Compliance: The Visitor Pattern allows developers to introduce new operations without modifying existing classes, satisfying OCP for operations.
- Separation of Concerns: Business operations (compression, auditing, size calculation) are cleanly separated from core domain data structures.
- Stable vs. Unstable Trade-off: The pattern excels when object structures are stable and static, while operations are dynamic and frequently evolving.
- Double Dispatch Mechanism: Relies on two dynamic method lookups (
accepton the element +visiton the visitor) to route execution correctly. - Method Overloading: Leverages method overloading in the visitor interface (
visit(TextFile),visit(ImageFile)) to handle different concrete types polymorphically. - Contrast with Strategy: Unlike Strategy—which handles different ways of doing the same thing—Visitor handles entirely new operations on fixed structures.