Enterprise Java

Exploring the Visitor Design Pattern with Java

Design patterns provide established approaches to recurring software design problems. Rather than being ready-made pieces of code, they describe how classes and objects can be organized to make an application easier to extend, maintain, and understand. Behavioral design patterns focus specifically on how objects communicate with one another and how responsibilities are distributed between them.

The Visitor Design Pattern is a behavioral pattern that addresses a situation where an application contains a collection of related object types and needs to perform different operations on those objects. Instead of placing every possible operation inside the object classes, the Visitor pattern moves those operations into separate visitor classes. The objects remain responsible for their core behavior while visitors handle additional operations that can change independently.

This approach can be useful in systems where the object structure remains relatively stable but the operations performed on that structure continue to grow. Document-processing systems, compilers, interpreters, static-analysis tools, and reporting systems are examples where this situation can occur.

This article takes a look at the Visitor Design Pattern using Java. It explains the problem that leads to the pattern, its main components, and several situations where it can be applied.

1. What Is the Visitor Design Pattern?

The Visitor Design Pattern is a behavioral design pattern that allows you to define new operations on a group of related object types without changing the classes of those objects. The main idea is to move operations into separate visitor classes. The objects being visited expose an accept() method, and the visitor provides a different method for each type of object it can process.

Suppose an application works with three types of files: PdfFile, WordFile, and ExcelFile. It may need to perform operations such as exporting files to HTML, calculating file sizes, generating reports, and performing security checks. Adding all these operations directly to the file classes can make them larger and harder to maintain.

The Visitor pattern separates these operations from the file classes. The file classes remain focused on representing their respective file types, while separate visitor classes handle operations such as HTML export, reporting, and security checks. This makes it easier to add new operations without continually modifying the existing file classes.

2. The Design Problem Behind the Visitor Pattern

Consider an application that represents different types of employees. The system may contain developers and managers, with each employee storing information appropriate to its role. A model could look like this:

public interface Employee {

}

A developer might contain a name and salary:

public class Developer implements Employee {

    private final String name;
    private final double salary;

    public Developer(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }
}

A manager could contain similar information together with the number of people they manage:

public class Manager implements Employee {

    private final String name;
    private final double salary;
    private final int teamSize;

    public Manager(String name, double salary, int teamSize) {
        this.name = name;
        this.salary = salary;
        this.teamSize = teamSize;
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public int getTeamSize() {
        return teamSize;
    }
}

At this stage, the classes are straightforward. The problem begins when the application needs to perform several different operations on these employees.

For example, the application might need to calculate bonuses, generate reports, calculate training requirements, perform compliance checks, and export employee information. One option would be to add methods for all of these operations to the employee classes. As the number of operations grows, however, the classes would gradually become responsible for functionality that does not naturally belong to them.

Another option would be to place type checks throughout the application. Code might repeatedly determine whether an employee is a developer or a manager and then execute different logic for each type. This approach can work for small applications, but it becomes difficult to maintain when many operations and employee types exist.

The Visitor pattern provides another approach. Instead of putting every operation inside Developer and Manager, the operations can be represented by visitor objects.

3. The Structure of the Visitor Pattern

The Visitor pattern consists of several cooperating components. Although the exact implementation can vary, most implementations contain an element abstraction, concrete elements, a visitor abstraction, concrete visitors, and a client that brings these components together.

The Element represents an object that can be visited. It normally defines an accept() method that receives a visitor.

public interface Employee { 
    
    void accept(EmployeeVisitor visitor); 
}

The Concrete Elements are the actual classes being processed. In this example, they are Developer and Manager. Each concrete element implements accept() and passes itself to the visitor.

@Override 
public void accept(EmployeeVisitor visitor) { 

   visitor.visit(this); 
}

The Visitor defines the operations that can be performed on the concrete elements. It normally contains one overloaded visit() method for each concrete element type.

public interface EmployeeVisitor {

    void visit(Developer developer);

    void visit(Manager manager);
}

The Concrete Visitors implement the operations declared by the visitor interface. One visitor might calculate bonuses, while another might generate employee reports.

Finally, the Client (Object Structure) creates the elements and visitors and invokes accept() on the elements. The client does not need to contain separate conditional logic for every employee type.

Together, these components create a structure in which new operations can be introduced by adding new visitor classes while leaving the existing elements unchanged.

4. Putting the Pattern Together with an Example

The previous sections described the individual components. The following example combines them into a complete implementation so that the interaction between the components can be seen in one place.

The first interface represents the element in the Visitor pattern:

public interface Employee { 
    
    void accept(EmployeeVisitor visitor); 
}

The visitor interface defines the operations that can be performed on the concrete employee types:

public interface EmployeeVisitor {

    void visit(Developer developer);

    void visit(Manager manager);
}

The Developer class implements the element interface and forwards itself to the visitor:

public class Developer implements Employee {

    private final String name;
    private final double salary;

    public Developer(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

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

The Manager class follows the same structure while providing its own manager-specific data:

public class Manager implements Employee {

    private final String name;
    private final double salary;
    private final int teamSize;

    public Manager(String name, double salary, int teamSize) {
        this.name = name;
        this.salary = salary;
        this.teamSize = teamSize;
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public int getTeamSize() {
        return teamSize;
    }

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

A bonus visitor can now implement the bonus rules without changing either employee class:

public class BonusVisitor implements EmployeeVisitor {

    @Override
    public void visit(Developer developer) {
        double bonus = developer.getSalary() * 0.10;

        IO.println(developer.getName() + " receives a developer bonus of $" + bonus);
    }

    @Override
    public void visit(Manager manager) {
        double bonus = manager.getSalary() * 0.15;

        IO.println(manager.getName() + " receives a manager bonus of $" + bonus);
    }
}

A separate visitor can generate employee reports:

public class ReportVisitor implements EmployeeVisitor {

    @Override
    public void visit(Developer developer) {
        IO.println("Developer: " + developer.getName() + ", salary: $" + developer.getSalary());
    }

    @Override
    public void visit(Manager manager) {
        IO.println("Manager: " 
                + manager.getName() 
                + ", salary: $" 
                + manager.getSalary() 
                + ", team size: " 
                + manager.getTeamSize());
    }
}

The client can now create employees and apply different visitors to them:

public class VisitorDesignPatternExample {

    public static void main(String[] args) {
        List<Employee> employees = List.of(
                new Developer("Thomas", 70000),
                new Manager("Eve", 100000, 8),
                new Developer("Charlie", 80000)
        );

        EmployeeVisitor bonusVisitor = new BonusVisitor();
        EmployeeVisitor reportVisitor = new ReportVisitor();

        IO.println("BONUSES");
        for (Employee employee : employees) {
            employee.accept(bonusVisitor);
        }
        IO.println();

        IO.println("EMPLOYEE REPORT");
        for (Employee employee : employees) {
            employee.accept(reportVisitor);
        }
    }
}

When the program is compiled and executed, the output will be similar to the following:

BONUSES
Thomas receives a developer bonus of $7000.0
Eve receives a manager bonus of $15000.0
Charlie receives a developer bonus of $8000.0

EMPLOYEE REPORT
Developer: Thomas, salary: $70000.0
Manager: Eve, salary: $100000.0, team size: 8
Developer: Charlie, salary: $80000.0

The example demonstrates the central idea of Visitor. The employee classes do not contain bonus or reporting logic. Those operations are represented by separate visitors, and the same employee objects can be processed by both visitors.

How the Visitor Pattern Uses Double Dispatch

The accept() method is one of the most important parts of the Visitor pattern because it enables a technique commonly known as double dispatch. Consider the following code:

Employee employee = new Developer("Thomas", 70000);
 
employee.accept(bonusVisitor);

The variable is declared as Employee, but the object stored in it is actually a Developer. Java therefore invokes the accept() implementation from Developer. Inside that method, the following statement executes:

visitor.visit(this);

At this point, this is a Developer, so the overloaded visit(Developer developer) method is selected. The interaction therefore involves two dispatches. The first dispatch determines which concrete element implementation should handle accept(). The second dispatch determines which visitor operation should handle that concrete element.

This is what allows the visitor to contain different behavior for Developer and Manager even though the client is working with the common Employee interface.

Adding Operations Without Changing Existing Elements

One of the strongest characteristics of Visitor is that a new operation can normally be added without modifying the existing element hierarchy. Suppose the application needs to determine the training requirements of employees. A new visitor can represent this operation:

public class TrainingVisitor implements EmployeeVisitor {

    @Override
    public void visit(Developer developer) {
        IO.println(developer.getName() + " requires advanced technical training.");
    }

    @Override
    public void visit(Manager manager) {
        IO.println(manager.getName() + " requires leadership training.");
    }
}

The existing Developer and Manager classes do not need to change. The client can simply create the visitor and apply it:

        EmployeeVisitor trainingVisitor = new TrainingVisitor();
        for (Employee employee : employees) {
            employee.accept(trainingVisitor);
        }

This is one of the main reasons to choose Visitor. When new operations are introduced frequently, keeping those operations in separate visitors prevents the element classes from continually expanding.

The Trade-Off When New Element Types Are Added

The flexibility provided by Visitor has an important trade-off. Although adding a new operation is relatively easy, adding a new element type requires changes to the visitor hierarchy. Suppose the application introduces an Intern employee. The visitor interface would need another method:

void visit(Intern intern);

Every concrete visitor would then need to implement that method. For example, BonusVisitor would need to decide how interns should be handled:

    @Override 
    public void visit(Intern intern) { 
        IO.println( intern.getName() + " does not receive a performance bonus." ); 
    
    }

The same change would be necessary in the report visitor, training visitor, and every other visitor. This means that Visitor favors applications where the element types are stable but operations change frequently. If new element types are constantly introduced, the pattern may create too much maintenance work.

5. Benefits of the Visitor Approach

The Visitor pattern provides several benefits when it is applied to the right problem. The most obvious benefit is that new operations can be introduced without modifying the existing element classes. This can be useful in large systems where those classes are already used by many parts of the application.

Visitor also helps group related operation logic. Instead of scattering the rules for one operation across several classes, the rules can be placed inside a single visitor. This can make the implementation easier to locate and maintain.

Another benefit is that visitors can work with concrete element types. The visitor interface explicitly defines the types it supports, which makes the relationship between an operation and the object hierarchy visible in the code.

The pattern can also reduce repeated type-checking logic. Rather than having many parts of the application perform their own instanceof checks, the visitor mechanism provides a consistent way to dispatch operations based on the concrete element.

6. Visitor Pattern Use Cases

The Visitor pattern can be useful in compiler and language tooling systems, where an abstract syntax tree contains different node types and multiple operations need to traverse that structure. Type checking, code generation, optimization, formatting, and static analysis can each be represented as separate visitors.

It can also be used in document-processing applications, where a stable document structure contains paragraphs, images, tables, headings, and other elements. Visitors can perform rendering, validation, indexing, statistics, or format conversion without placing all of those responsibilities inside the document elements.

Another use case is static code analysis. Different visitors can inspect a source-code structure for different categories of problems, allowing individual analyses to evolve independently.

Visitor can also fit reporting and analytics systems where a known collection of domain objects must support several calculations or reporting operations. Separate visitors can implement different reports while the domain objects remain focused on representing the underlying data.

Visitor can also be applied to structured data transformations when an application repeatedly processes a stable object hierarchy in different ways. This can include converting an object structure into different representations, calculating aggregate information, or validating the structure against different rules.

7. Conclusion

The Visitor Design Pattern provides a structured way to separate operations from the objects on which those operations operate. It introduces an element interface with an accept() method and a visitor interface containing type-specific visit() methods. Together, these mechanisms allow an operation to be selected according to the concrete type of the element.

The pattern is particularly suited to applications where the object structure is stable but the number of operations performed on that structure continues to increase. Document processing, compilers, syntax trees, static analysis, and reporting systems are examples where this design can make sense.

The main trade-off should always be considered before using the pattern. Visitor makes it easier to introduce new operations, but it makes introducing new element types more expensive because every visitor must account for the new type.

For small applications, ordinary polymorphism may be simpler and easier to understand. Visitor becomes more valuable when there is a clear need to support many independent operations over a relatively stable set of object types.

The most important part of using Visitor effectively is therefore not memorizing its class diagram or the accept() and visit() methods. It is recognizing the type of change the application is expected to experience. When operations are likely to change more frequently than the underlying object structure, Visitor provides a way to keep those operations separate while maintaining a consistent mechanism for processing the objects.

8. Download the Source Code

Download
You can download the full source code of this example here: the visitor design pattern and its clean operations across complex object structures

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button