Link Search Menu Expand Document

The Visitor Design Pattern

The Visitor Design Pattern is designed to separate the algorithm from the object and to apply it to a group of similar types.


What problems does it solve?

It provides a mechanism to separate the algorithm from the object and to apply it to many different types.

Glossary:

  • Visitor - an interfaces or abstract class to define the visitor behavior (the visit methods)
  • Concrete Visitor - implements the concrete operations/ algorithms
  • Visitable - declares the behavior of the classes that can be “visited”, acts as an entry point for visitors
  • Concrete Visitable - the concrete classes should implement the behavior when “visited” by the visitor
  • Object Structure - a collection or a complex structure, that contains the “elements” that can be visited

You can use it with a collection/ complex structure or without.

Pros:

  • you can define new operation without changing current implementation of the classes
  • you can add new operation without changing the code of the class hierarchy
  • algorithm implementation will be externalized and more reusable
  • implements the open/closed principle of the SOLID design principles

Cons:

  • you should know the return type in advance
  • hard to modify the return type later

One Type that may apply many algorithms = Strategy Many Types that can apply many algorithms = Visitor

How to recognize it?

If there are two different abstract/interface types, which have methods and the one actually calls the method of the other to execute the desired strategy on it.

// use the complex shape to calculate the total area
ComplexShape c1 = new ComplexShape();
c1.add(new Circle(3));
c1.add(new Circle(6));
c1.add(new Circle(9));
c1.add(new Rectangle(12, 2));
c1.add(new Hexagon(2));
c1.calcTotalArea();

Scenarios

  • When you have to perform the same operation on multiple classes, that build a complex structure, but you could not implement it in the parent class, because not all of the subclasses support it. In this case, you can extract the operation/ algorithm in external class and reuse it, where applicable.
  • When you don’t want to add a method to a parent class

Examples from Java API

javax.lang.model.element.AnnotationValue and AnnotationValueVisitor
javax.lang.model.element.Element and ElementVisitor
javax.lang.model.type.TypeMirror and TypeVisitor
java.nio.file.FileVisitor and SimpleFileVisitor
javax.faces.component.visit.VisitContext and VisitCallback

Scenarios

  • If you want to apply same operation on all elements of some aggregate structure, but elements might be of different types and the algorithm may vary for the concrete types
  • If many distinct and unrelated operations need to be performed on objects in an object structure
  • To define new operations over the elements of a structure, but you don’t want to modify the existing classes

Example 1

We need an app that can calculate the area of dynamic complex shapes, built from other “simple” shapes. Later, we will extend the functionality and some of the shapes will do specific operations like “print”.

Source Code

1). Create an visitable interface for the Area Calculator, that knows how to “accept” visitors ot this type

public interface VisitablePrint {

    void accept(VisitorPrint visitor);
}

2). Create the “simple” shapes we are going to support and implement the interface

2.1).Circle

@ToString
public class Circle implements VisitableAreaCalc {

    @Getter
    private  double radius;

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

    @Override
    public double accept(VisitorAreaCalc visitor) {
        return visitor.visit(this);
    }
}

2.2).Hexagon

@ToString
public class Hexagon implements VisitableAreaCalc {

    @Getter
    private int side;

    public Hexagon(int side) {
        this.side = side;
    }

    @Override
    public double accept(VisitorAreaCalc visitor) {
        return visitor.visit(this);
    }
}

2.3). Rectangle

@ToString
public class Rectangle implements VisitableAreaCalc {

    @Getter
    private int width;

    @Getter
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double accept(VisitorAreaCalc visitor) {
        return visitor.visit(this);
    }

    public void accept(VisitorPrint visitor) {
        visitor.visit(this);
    }
}

3). Add the implementation of the Area Calculator Visitor - this is the implementation of the algorithms, extracted from the classes

public class VisitorAreaCalcImpl implements VisitorAreaCalc {

    @Override
    public double visit(Rectangle shape) {
        double area = shape.getWidth() * shape.getHeight();
        System.out.println("Area of " + shape + " is " + area);
        return area;
    }

    @Override
    public double visit(Circle shape) {
        double area = Math.PI * Math.pow(shape.getRadius(), 2);
        System.out.println("Area of " + shape + " is " + area);
        return area;
    }

    @Override
    public double visit(Hexagon shape) {
        double area = (3 * Math.sqrt(3) * Math.pow(shape.getSide(), 2)) / 2;
        System.out.println("Area of " + shape + " is " + area);
        return area;
    }
}

4). Let’s extend the functionality and add a visitor for printing the shape

public interface VisitorPrint {

    void visit(Rectangle rectangle);
}

5). Add the visitable interface for printing

public interface VisitablePrint {

    void accept(VisitorPrint visitor);
}

6). Implement the visitable for a specific shape

@ToString
public class Rectangle implements VisitableAreaCalc, VisitablePrint {

    @Getter
    private int width;

    @Getter
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double accept(VisitorAreaCalc visitor) {
        return visitor.visit(this);
    }

    public void accept(VisitorPrint visitor) {
        visitor.visit(this);
    }
}

7). Implement the print mechanism

public class VisitorPrintImpl implements VisitorPrint {
    @Override
    public void visit(Rectangle rectangle) {
        for (int h = 0; h < rectangle.getHeight(); h++) {
            for (int w = 0; w < rectangle.getWidth(); w++) {
                System.out.print("*");
            }
            System.out.println();
        }
    }
}

8). Create a complex shape

public class ComplexShape {

    private List<VisitableAreaCalc> items = new ArrayList<>();

    public void add(VisitableAreaCalc shape) {
        this.items.add(shape);
    }

    public void calcTotalArea() {
        VisitorAreaCalc areaCalculator = new VisitorAreaCalcImpl();

        double area = 0;
        for (VisitableAreaCalc shape : this.items) {
            area += shape.accept(areaCalculator);
        }

        System.out.println("Total area: " + area);
    }
}

9). Create a demo class

public class _Main {

    public static void main(String[] args) {
        // calculate the area of a single shape
        (new Circle(3)).accept(new VisitorAreaCalcImpl());

        // print a single shape
        (new Rectangle(12, 4)).accept(new VisitorPrintImpl());

        // use the complex shape to calculate the total area
        ComplexShape c1 = new ComplexShape();
        c1.add(new Circle(3));
        c1.add(new Circle(6));
        c1.add(new Circle(9));
        c1.add(new Rectangle(12, 2));
        c1.add(new Hexagon(2));
        c1.calcTotalArea();
    }
}

Output:

Area of Circle(radius=3.0) is 28.274333882308138
************
************
************
************
Area of Circle(radius=3.0) is 28.274333882308138
Area of Circle(radius=6.0) is 113.09733552923255
Area of Circle(radius=9.0) is 254.46900494077323
Area of Rectangle(width=12, height=2) is 24.0
Area of Hexagon(side=2) is 10.392304845413264
Total area: 430.2329791977272