Skip to main content
Create your own
Lesson illustration

Visitor Pattern for Extending Functionality

Hello! Welcome to your next lesson on behavioral design patterns.

In our last session, we covered the Template Method pattern. We saw how it uses inheritance to define a fixed algorithm skeleton in a base class, while allowing subclasses to provide specific implementations for certain steps. This is ideal when the overall process is consistent, but the details vary.

Today, we shift our focus to a different, yet equally powerful pattern. Our goal is to apply the Visitor pattern to add new operations to classes without modifying them. This pattern is a fantastic tool for managing complexity when you have a stable set of classes but an ever-growing list of operations you need to perform on them.

This directly addresses a common challenge in large applications, including the kind you build with Spring Boot, where you might have a rich domain model and need to add new functionalities like reporting, exporting, or validation without cluttering your core domain objects.

The Problem: When New Features Mean Modifying Old Code

Imagine you're working on a system that deals with different types of client accounts: Bank, Company, and Resident. These are your core domain classes.

Now, the marketing department wants you to implement a feature to send promotional emails. The email content must be tailored to the client type:

  • Banks get an ad for theft insurance.
  • Companies get an ad for fire insurance.
  • Residents get an ad for medical insurance.

A straightforward approach would be to add a sendAdEmail() method to a common base class or interface and implement it in each client class. This works, but what happens next week when the legal department wants you to generate a compliance report for each client type? You'd have to go back and modify all the client classes again to add a generateComplianceReport() method.

This cycle of modifying stable classes for every new operation leads to several problems:

  • Violation of the Single Responsibility Principle (SRP): Your client classes, which should only be responsible for holding client data, are now also responsible for email logic, reporting logic, and who knows what else.
  • Violation of the Open/Closed Principle (OCP): Your classes are not closed for modification. Every new feature request forces you to reopen and change them, risking the introduction of bugs into stable code.
  • Code Bloat: The core domain classes become cluttered with unrelated business logic.

The core issue is often revealed by code that looks like this in a service class:

// A common code smell in a service layer
public void performOperation(Client client) {
    if (client instanceof Bank) {
        // Logic for Bank
    } else if (client instanceof Company) {
        // Logic for Company
    } else if (client instanceof Resident) {
        // Logic for Resident
    }
    // ... and so on for every new client type
}

This instanceof chain is a major red flag for maintainability.

Visitor pattern with a real-world example

The article 'Visitor pattern with a real-world example' from Javamentor does an excellent job of describing this exact problem. It frames it in the context of handling different form types, a scenario you might encounter in web applications.

Read the first four sections, from the beginning up to 'How to solve this problem with the visitor pattern'. Focus on how the initial solution with instanceof checks leads to huge, unmaintainable methods and violates the Open/Closed Principle.

The Solution: Separating Operations with the Visitor Pattern

The Visitor pattern solves this by decoupling the operations from the objects they operate on. It achieves this by creating two separate class hierarchies:

  1. The Element Hierarchy: The objects that need to be "visited" (e.g., Bank, Company, Resident).
  2. The Visitor Hierarchy: The operations that will be performed on the elements (e.g., EmailVisitor, ReportVisitor).

The Visitor Pattern Explained and Implemented in Java | Behavioral Design Patterns | Geekific

To see how this separation works conceptually, let's watch this introductory video from Geekific. It uses an insurance client example that closely matches the problem we just discussed.

Watch from the beginning up to 02:32. Pay attention to how the logic from the client classes is extracted and moved into a dedicated 'Visitor' class.

How It Works: The "Double Dispatch" Technique

So, how do we connect the visitor to the right element without using instanceof? The answer is a clever technique called double dispatch.

Here’s the sequence:

  1. The client code calls element.accept(visitor). This is the first dispatch. The JVM selects the correct accept() method based on the actual type of the element at runtime (e.g., Bank.accept() vs Company.accept()).
  2. Inside the accept() method, the element immediately calls visitor.visit(this). This is the second dispatch. Since the visitor knows the concrete type of this (e.g., it knows it's a Bank object), the JVM can select the correct overloaded visit() method (e.g., visit(Bank bank)).

This two-step handshake allows us to execute the correct operation for the correct type, all without a single instanceof check.

The Visitor Pattern Explained and Implemented in Java | Behavioral Design Patterns | Geekific

The same Geekific video explains double dispatch very clearly.

Continue watching from 02:32 to 03:28. Focus on understanding why simple method overloading doesn't work and how delegating the choice back to the object (via accept) solves the problem.

The Structure of the Visitor Pattern

Let's formalize the components involved in this pattern.

UML Class Diagram for Visitor Pattern in a Shopping Cart Example
This UML diagram shows the key participants in the Visitor pattern. The `ItemElement` interface has an `accept` method, and the `ShoppingCartVisitor` has overloaded `visit` methods for each concrete element (`Book`, `Fruit`). This structure separates the items from the operations performed on them, such as calculating costs.

The main participants are:

  • Visitor (Interface): Declares a visit() method for each ConcreteElement type in the object structure. For example, visit(Bank bank), visit(Company company).
  • ConcreteVisitor (Class): Implements the Visitor interface and provides the actual logic for the operation for each element type.
  • Element / Visitable (Interface): Declares an accept() method that takes a Visitor object as an argument.
  • ConcreteElement (Class): Implements the Element interface. The accept() method's implementation is always the same: visitor.visit(this);.

Implementation in Java

Now let's walk through a complete implementation in Java. We'll use a tax calculation example, where different product types (liquor, tobacco, necessities) are taxed differently. This is a classic use case for the Visitor pattern.

Visitor Design Pattern

This comprehensive video by Derek Banas provides a full, step-by-step implementation. It's excellent for seeing how all the pieces fit together in code.

Watch from 02:43 to the end. I recommend having your IDE open to follow along. Pay close attention to these key steps: Defining the interfaces (Visitor, Visitable): The contracts for our two hierarchies (02:43-03:45, 06:06-06:30). Implementing a ConcreteVisitor (TaxVisitor): This class contains the different tax calculation logics (03:45-06:06). Implementing ConcreteElements: How the Liquor, Tobacco, and Necessity classes implement the accept method (06:30-09:02). Adding a new operation (TaxHolidayVisitor): Notice how this is done by simply creating a new visitor class, without touching any of the product classes. This is the pattern's biggest win! (09:02-09:55). Client Code: How to use the visitors on the element objects (09:55-end).

As you saw in the video, adding a completely new tax scheme (TaxHolidayVisitor) was as simple as creating one new class. The original Liquor, Tobacco, and Necessity classes remained untouched, perfectly demonstrating the Open/Closed Principle in action.

Test your understanding!

You are designing a simple document model with two element types: Paragraph and Image. You need to implement an operation to export the document to HTML.

Using the Visitor pattern, what interfaces and classes would you create? What would the key methods look like in the Paragraph class and the HtmlExportVisitor class?

Show answer

You would create the following:

  1. DocumentElement (interface):
    interface DocumentElement {
        void accept(DocumentVisitor visitor);
    }
    
  2. Paragraph and Image (concrete classes):
    class Paragraph implements DocumentElement {
        // ... paragraph content ...
        @Override
        public void accept(DocumentVisitor visitor) {
            visitor.visit(this); // Double dispatch
        }
    }
    
    class Image implements DocumentElement {
        // ... image data ...
        @Override
        public void accept(DocumentVisitor visitor) {
            visitor.visit(this); // Double dispatch
        }
    }
    
  3. DocumentVisitor (interface):
    interface DocumentVisitor {
        void visit(Paragraph paragraph);
        void visit(Image image);
    }
    
  4. HtmlExportVisitor (concrete class):
    class HtmlExportVisitor implements DocumentVisitor {
        @Override
        public void visit(Paragraph paragraph) {
            System.out.println("<p>" + paragraph.getContent() + "</p>");
        }
    
        @Override
        public void visit(Image image) {
            System.out.println("<img src='" + image.getSource() + "' />");
        }
    }
    

The Downside: Adding New Elements is Hard

The Visitor pattern is not a silver bullet. While it makes adding new operations easy, it makes adding new elements difficult.

Imagine you want to add a new Video element to your document model. You would have to go back and update every single existing visitor (HtmlExportVisitor, PlainTextVisitor, etc.) to add a visit(Video video) method. This breaks the Open/Closed Principle for the visitor hierarchy.

Visitor Design Pattern in Java

The Baeldung article on the Visitor pattern provides a concise explanation of this important trade-off.

Read section '6. Downsides'. This is a critical consideration for deciding when to use the Visitor pattern.

Therefore, the Visitor pattern is most suitable when you have a stable hierarchy of element classes but anticipate needing to add many new operations over time.

Conclusion

In this lesson, we've explored the Visitor pattern, a powerful behavioral pattern for separating algorithms from the objects they operate on.

Key Takeaways:

  • Purpose: To add new operations to an object structure without modifying the classes of those objects.
  • Core Problem Solved: It avoids long, brittle instanceof chains and helps adhere to the Open/Closed and Single Responsibility principles.
  • Mechanism: It relies on double dispatch, a two-step process involving an accept(Visitor) method in the element and overloaded visit(Element) methods in the visitor.
  • Primary Benefit: Makes it easy to add new operations (new visitors).
  • Primary Drawback: Makes it difficult to add new element types, as all existing visitors must be modified.

In our next lesson, we will look at the Memento pattern. Where Visitor is about adding external operations, Memento is about an object's internal state—specifically, how to capture and restore it without breaking encapsulation, which is fundamental for implementing features like undo/redo or saving game states.

Can't find a good explanation? Sign up and we'll make it for you

Sign up