3.6 - Visitor Pattern : Behavioral Design Patterns

The Visitor Pattern is a behavioral design pattern that allows adding new operations to a set of objects (elements) without modifying their structure. The pattern involves creating a visitor class that implements operations for different types of objects. This pattern promotes open/closed principle: the element classes remain unchanged, and new operations can be added via new visitor implementations.

This pattern is particularly useful when dealing with objects that belong to different classes but share common operations, as it separates the algorithm from the object structure.

Key Components of the Visitor Pattern:

  1. Visitor Interface: Declares a visit method for each type of element in the system.

  2. Concrete Visitor: Implements the visit operations for each element type.

  3. Element Interface: Defines an accept() method that takes a visitor as an argument.

  4. Concrete Elements: Implement the accept() method, which calls the appropriate visit() method on the visitor object.

  5. Client: Uses the visitor to apply different operations on the elements.

Code Example

Here’s an implementation of the Visitor Pattern using renamed class and function names for better understanding.

1. Visitor Interface

The ShapeVisitor interface declares visit methods for different types of shapes.

// Visitor Interface
interface ShapeVisitor {
    void calculate(CircleShape circle);
    void calculate(SquareShape square);
}

2. Concrete Visitor

The AreaCalculatorVisitor class implements the ShapeVisitor interface and calculates the area of each shape.

// Concrete Visitor
class AreaCalculatorVisitor implements ShapeVisitor {
    @Override
    public void calculate(CircleShape circle) {
        double area = Math.PI * circle.getRadius() * circle.getRadius();
        System.out.println("Area of Circle: " + area);
    }

    @Override
    public void calculate(SquareShape square) {
        double area = square.getSide() * square.getSide();
        System.out.println("Area of Square: " + area);
    }
}

3. Element Interface

The GeometricShape interface defines the accept() method that will be used by the visitor.

// Element Interface
interface GeometricShape {
    void accept(ShapeVisitor visitor);
}

4. Concrete Elements

The CircleShape and SquareShape classes implement the GeometricShape interface and their respective accept() methods.

// Concrete Element - Circle
class CircleShape implements GeometricShape {
    private double radius;

    public CircleShape(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    @Override
    public void accept(ShapeVisitor visitor) {
        visitor.calculate(this);  // Pass the CircleShape object to the visitor
    }
}

// Concrete Element - Square
class SquareShape implements GeometricShape {
    private double side;

    public SquareShape(double side) {
        this.side = side;
    }

    public double getSide() {
        return side;
    }

    @Override
    public void accept(ShapeVisitor visitor) {
        visitor.calculate(this);  // Pass the SquareShape object to the visitor
    }
}

5. Client

The VisitorDemo class demonstrates how to use the visitor pattern to calculate areas of different shapes.

// Client Class
public class VisitorDemo {
    public static void main(String[] args) {
        // Array of geometric shapes
        GeometricShape[] shapes = { 
            new CircleShape(5.0), 
            new SquareShape(4.0) 
        };

        // Create the visitor object
        ShapeVisitor areaCalculator = new AreaCalculatorVisitor();

        // Apply the visitor to each shape
        for (GeometricShape shape : shapes) {
            shape.accept(areaCalculator);
        }
    }
}

Explanation:

  1. ShapeVisitor Interface: This interface defines two methods, calculate(CircleShape circle) and calculate(SquareShape square), which are used to perform specific operations on the respective shape types.

  2. AreaCalculatorVisitor: This class implements the ShapeVisitor interface and defines the logic for calculating the area of a circle and a square.

  3. GeometricShape Interface: This interface contains an accept() method, which takes a visitor object and allows the shape to delegate operations to the visitor.

  4. CircleShape and SquareShape: These classes implement the GeometricShape interface. The accept() method calls the appropriate calculate() method from the visitor.

  5. Client (VisitorDemo): The client creates a collection of shapes (CircleShape and SquareShape) and applies the AreaCalculatorVisitor to compute the area of each shape using the accept() method.

Benefits of the Visitor Pattern:

  1. Open/Closed Principle: The pattern allows you to add new operations (via new visitors) without modifying the existing element classes. This makes the system open for extension but closed for modification.

  2. Separation of Concerns: Visitor logic is separated from the element structure, leading to cleaner, more maintainable code. The visitor focuses on the operation, while the element focuses on its internal data.

  3. Flexibility: You can introduce new visitors to perform different operations on the same elements without altering the element's structure or code.

  4. Easy Maintenance: Since the logic for each operation is centralized in a visitor, it becomes easier to update and maintain operations related to the objects.

Real-World Use Cases:

  • Compilers: In compilers, the visitor pattern is used to traverse abstract syntax trees (AST) and apply operations such as type checking, code generation, or optimization on different nodes.

  • File Systems: Visitors can be used to perform operations such as searching, deleting, or compressing files in a file system.

  • UI Components: In UI frameworks, the visitor pattern can be used to apply various actions (e.g., rendering, resizing) on different components like buttons, text fields, or windows.

Conclusion:

The Visitor Pattern is a powerful design pattern that allows for adding new functionality to existing class hierarchies without modifying the classes themselves. By utilizing visitors, you can decouple operations from the data structures they operate on, promoting flexibility and adherence to the open/closed principle. This pattern is especially useful when you need to perform several unrelated operations across a collection of objects without altering their code.