DEV Community

loading...
Cover image for The Visitation of Visitor Pattern: How it makes your software more useful

The Visitation of Visitor Pattern: How it makes your software more useful

minhthanh3145 profile image Tô Minh Thành ・11 min read

Original post here

The visitor pattern is a behavior design pattern, which means it's a way for objects to interact in order to solve a problem. The problem for the visitor pattern is to add functionalities to a class hierarchy without having to modify every single class for every single functionality. This sounds abstract, but I will try to be more concrete as we proceed.

The constraint that you may not modify code of any class within the hierarchy, like the case of third-party library classes, yet still allow for additional functionalities that are relevant to the classes to be added, is actually a constraint frequently met by library designers.

In this post, my goal is to perhaps make sense of the

The platitudinous Problem

The problem we will use for this post is, without a doubt, very platitudinous: How to design a hierarchy of objects to represent shapes with certain computable aspects like area and volume.

Did someone say my name ? Also, shapes ? Again ?

I often find such problems to be too simple and therefore delude the purpose of design patterns. However, a real world example would perhaps contain too many extra functional and non-functional requirements that would also delude the focus on design patterns. On rare occasions, platitudes may be re-interpreted in ways that are helpful and delightful. Not saying that this post is definitely either of these things, but hopefully it would be.

Let's start even smaller. On the computable aspects, let's say you're not mathematically gifted. You only know properties of individual shapes (rectangles, circles, etc) and the formula to calculate their area only if they are described in a requirement. Also, your team unfortunately works in a way that all relevant requirements don't come to you at the same time, but scattered through time and space and are influenced by multiple parties. Now that sounds more realistic.

So as you are looking melancholically at your female co-worker while leaning against something, the first requirements comes.

https://undraw.co/thankful

It is to calculate the area of the shapes using their respective attributes and formula.

A solution Without visitor pattern

So a no-brainer solution is, well, a no-brainer. Ignore the static, I find them rather annoying, but for the sake of having a main function I will be tolerant.

The solution is just literally a hierarchy of shapes in the most obvious way imaginable, and the computations, which are represented by literal values (I aint mathematically gifted y'all), are put into the appropriate classes.

public class WithoutVisitorPattern {

    public abstract static class Shape {
        public abstract int getArea();
    }

    public static class Rectangle extends Shape {
        @Override
        public int getArea() {
            return 0;
        }
    }

    public static class Circle extends Shape {
        @Override
        public int getArea() {
            return 1;
        }
    }

    public static class Shape1 extends Shape {
        @Override
        public int getArea() {
            return 2;
        }
    }

    public static class Shape2 extends Shape {
        @Override
        public int getArea() {
            return 3;
        }
    }

    public static void main(String\[\] args) {
        List<Shape> shapes = new ArrayList<>();
        shapes.add(new Rectangle());
        shapes.add(new Circle());
        shapes.add(new Shape1());
        shapes.add(new Shape2());
        System.out.println(calculateShapeArea(shapes));
    }

    public static int calculateShapeArea(List<Shape> shapes) {
        int result = 0;
        for(Shape shape : shapes) {
            result += shape.getArea();
        }
        return result;
    }
}

And frankly, it works well. With the current scope of requirements, any more complexity would be unnecessary. Minimalism is a respectable pursuit.

But life isn't that simple. After a while, your team decide to distribute the above code into production. This can either mean this code live in an official release of your application, or live inside a version of a library that your clients will use. Let's be dramatic and assume the second scenario. You distribute your library as a JAR file to your clients. Your clients use this library to calculate the area of the shapes to do some, uh, mathematical mission-critical stuffs.

But as they use it, their mathematics extend to well beyond area of shapes, and thus they demand that your library accommodates their new requirement - which is to calculate volume of the shapes. Okay, so with the above solution, in order to accommodate a new method called getVolume for every shape, you must add the signature to the base class, and then for every subclass, you also need to add the signature getVolume.

Because your team supports an extensive library for shapes, there are more than 23 shapes in the hierarchy. 23 is my current age by the way. Anyway, you would need to add 23 getVolume methods into each of these 23 classes. Did I mention 23 is my age ? Okay enough with these.

Why the problem matters ?

You may be thinking: "I see that this is the problem that you mentioned earlier: To add additional functionalities relevant to the class hierarchy without modifying the class hierarchy. But hey, you have to add 23 different getVolume methods either way, because the formula for each of these 23 shapes is different from each other, so what's the harm in placing these methods in the class hierarchy itself, compared to an alternative. Sure, it'd be more elegant and also adhere to the so-called Open-Closed Principle. But, the first one is subjective, the second one sounds like Authority Bias to me. "

Okay, maybe you won't be thinking exactly that, but something like that is a reasonable objection. However, one of the important characteristics of good software designs is to accommodate changes.

Because other software depend on your software

Another software team is working on a different mathematics project and they found your software to be very useful, but lacking some shapes that are particular to their projects. So they use the JAR file you distribute, extend the subclass Shape and implement another 23 shapes that work for them.

Now the requirement for volume of shapes comes, among other requirements such as bug fixes and optimizations. You simply add 23 getVolume methods to each of the class in the hierarchy and release the new version of the library with attractive bug fixes and optimizations. However, because the software team above also extends from your base class, they would also need to add getVolume into their 23 shapes in order to use the new version. The risk tends to pose difficulty in justifying the library upgrade, so likely they won't use the new version(s). For you, this means that people are not receiving values from your new releases. What's the point of software if it's not to continue to provide values ?

This gets worse when it cascades to further software teams that depend on the software team above, and I think this is enough to tell you that your design, as it currently stands, does not accommodate changes very efficiently. This problem suggests that another solution is needed to keep the propagation of change to be minimal.

Because software ceases to evolve

It is not an exception that software components cease to evolve. They may stop because the budget runs out so no one is maintaining the library anymore. They may stop because of a natural disaster that kills everyone who works on that library specifically. Whatever the cause is, a consequence is that you and your team, as the library developers, can no longer accommodate new changes to the library. That responsibility unfortunately will fall into other software teams who extend your library. However, just because your library is not maintained doesn't mean it is not useful anymore.

So when your project is no longer maintained, new functionalities can no longer be added into the hierarchy inside your library. The software team who decides to use your library to reap its existing benefits must create a new class hierarchy that use your class hierarchy to add functionalities. There is no other way. This, unfortunately, would mean that new functionalities would have to be used differently than old functionalities, which confuse new members of the team, which adds more to the cost. It's also annoying to explain that we have to code this way because of, quote, unquote, legacy code.

A solution that solves the problem

So after we have seen that adding functionalities by directly modifying code of the hierarchy is not a design that would facilitate useful, long-lasting software libraries.

So what's the solution ? It is to do what future software teams would have to do if they no longer can modify your class hierarchy. Create another class hierarchy and make it do the computations, instead of having the computations inside the Shape hierarchy.

From the above solution without visitor pattern, if we go along with the idea of delegating the computations to another hierarchy, we would end up with something like this.

public class EvolutionToVisitorPattern {

    public abstract static class Shape {
        public abstract int getArea(AreaCalculator areaCalculator);
    }

    public static class AreaCalculator {
        public int getAreaOf(Rectangle shape) {
            return 0;
        }
        public int getAreaOf(Circle shape) {
            return 1;
        }
        public int getAreaOf(Shape1 shape) {
            return 2;
        }
        public int getAreaOf(Shape2 shape) {
            return 3;
        }
    }

    public static class Rectangle extends Shape {
        @Override
        public int getArea(AreaCalculator areaCalculator) {
            return areaCalculator.getAreaOf(this);
        }
    }

    public static class Circle extends Shape {
        @Override
        public int getArea(AreaCalculator areaCalculator) {
            return areaCalculator.getAreaOf(this);
        }
    }

    public static class Shape1 extends Shape {
        @Override
        public int getArea(AreaCalculator areaCalculator) {
            return areaCalculator.getAreaOf(this);
        }
    }

    public static class Shape2 extends Shape {
        @Override
        public int getArea(AreaCalculator areaCalculator) {
            return areaCalculator.getAreaOf(this);
        }
    }

    public static void main(String\[\] args) {
        List<Shape> shapes = new ArrayList<>();
        shapes.add(new Rectangle());
        shapes.add(new Circle());
        shapes.add(new Shape1());
        shapes.add(new Shape2());
        System.out.println(calculateShapeAttribute(shapes));
    }

    public static int calculateShapeAttribute(List<Shape> shapes) {
        AreaCalculator areaCalculator = new AreaCalculator();
        int result = 0;
        for(Shape shape : shapes) {
            result += shape.getArea(areaCalculator);
        }
        return result;
    }
}

This solution creates a class called AreaCalculator which implements the computation to get area of the shapes. Each call to getArea is now given an instance of AreaCalculator and simply delegate to AreaCalculator to compute and return the area.

At first glance, the obvious problem seems to be code duplication. If we look at the patterns of text in the child classes, it's not unreasonable to think they're duplication, and instead we can just modify the base class to:

    public abstract static class Shape {
        public int getArea(AreaCalculator areaCalculator) {
            return areaCalculator.getAreaOf(this);
        }
    }

However, that won't work, because this is of type Shape which the AreaCalculator does not have a method that accepts. To fix this, we may want to modify AreaCalculator to:

        public int getAreaOf(Shape shape) {
            if(shape instanceof Rectangle) {
                return aPrivateMethodToCalculatAreaForRectangle(shape);
            }
            if(shape instanceof Shape1) {

            }
            // 21 more to go :(
        }

But this defeats the purpose. Suppose that another software team want to extend AreaCalculator into their own class because they want to optimize the current calculation of area for Rectangle. Too bad, they can't do it because finding the right shape and delegating to the right private method happens within the same function. Therefore doing it this way would prevent customization of existing functionalities.

So we want to maintain the solution as it is, because then another software team only need to extends AreaCalculator and override public int getAreaOf(Rectangle shape) to provide their own customized implementation.

The second aching problem is that, each method is receiving an AreaCalculator instance, this seems unnecessary, can we just inject the instance into the class constructor and then re-use it. So, start with the base class.

    public abstract static class Shape {
        protected AreaCalculator calculator;
        Shape(AreaCalculator calculator) {
            this.calculator = calculator;
        }
        public abstract int getArea(AreaCalculator areaCalculator);
    }

Wait a minute, there are red marks !

Turns out that, we have to add a constructor for all classes extending Shape in order to do it this way. Suddenly, it doesn't seem to worth the efforts anymore. With every instance of AreaCalculator you save from a method, the same instance must go into the constructor. However, saying that it takes the same efforts for both approaches does not give us information to choose one over another.

A practical reason would be that, if we decide to give an instance AreaCalculator to all shapes in the constructor, it implies that we have to make an instance of AreaCalculator before we can use any shape. The construction of an AreaCalculator instance that must do intense computations in an optimized way would be costly. When such an AreaCalculator instance is constructed, an implementation that maximizes optimization would have to read CPU resources, retrieving information from other parts of the application in order to determine the most efficient area computation strategy.

On the other hand, our Shape subclasses represent lightweight objects that contain information about the shapes at hand. We even delegate computations to another class. If we have to make an instance of AreaCalculator before every shape is created, it turns out to be sub-optimal, because we are creating a very costly object that we don't use yet. We can't make the assumption that the area computation would occur right after these shapes are created. Therefore, delaying the construction of AreaCalculator right to the moment when the shapes need it, is the optimal strategy. For this reason, we maintain our solution as it is.

A solution with visitor pattern

With all that said, the visitor pattern is actually half-way achieved. In order to support a volume computation, we can just create the method getVolume for each of the class in the class hierarchy and delegate the logic to a VolumeCalculator. However, we can notice that AreaCalculator and VolumeCalculator actually shares the same signatures, and they are used in the same way by the subclasses of Shape, so they can be grouped together into a new inheritance hierarchy. This new hierarchy has the base class Calculator which has a method calculate that is used by getArea and getVolume.

So now, getVolume and getArea has the same signature and logic: Receiving a Calculator instance, call Calculator.calculate(this) and return the result, therefore they are practically just one method in the class Shape hierarchy. Let's this method be called getComputedValueFrom(Calculator).

Now your Shape use Calculator as something that it merely passes this to and receive a value, it's not strictly just about computation anymore, because we can perform additional logic and algorithm inside Calculator.calculate as well. Whenever we see an abstraction that does multiple things, it's a good idea to name it something generic so that its subclass can take on concrete meanings. With this principle, we replace Calculator by Visitor, getComputedValueFrom with accept, and calculate method with visit. Voila. You end up with the visitor pattern.

public class WithVisitorPattern {

    public static abstract class ShapeVisitor {
        public abstract int visit(Rectangle shape);
        public abstract int visit(Circle shape);
        public abstract int visit(Shape1 shape);
        public abstract int visit(Shape2 shape);
    }

    public static class AreaShapeVisitor extends  ShapeVisitor {
        public int visit(Rectangle shape) { return 0;}
        public int visit(Circle shape) { return 1;}
        public int visit(Shape1 shape) { return 2;}
        public int visit(Shape2 shape) { return 3;}
    }

    public static class VolumeShapeVisitor extends ShapeVisitor {
        public int visit(Rectangle shape) { return -0;}
        public int visit(Circle shape) { return -1;}
        public int visit(Shape1 shape) { return -2;}
        public int visit(Shape2 shape) { return -3;}
    }

    public abstract static class Shape {
        public abstract int accept(ShapeVisitor visitor);
    }

    public static class Rectangle extends Shape {
        @Override
        public int accept(ShapeVisitor visitor) {
            return visitor.visit(this);
        }
    }

    public static class Circle extends Shape {
        @Override
        public int accept(ShapeVisitor visitor) {
            return visitor.visit(this);
        }
    }

    public static class Shape1 extends Shape {
        @Override
        public int accept(ShapeVisitor visitor) {
            return visitor.visit(this);
        }
    }

    public static class Shape2 extends Shape {
        @Override
        public int accept(ShapeVisitor visitor) {
            return visitor.visit(this);
        }
    }

    public static void main(String\[\] args) {
        List<Shape> shapes = new ArrayList<>();
        shapes.add(new Rectangle());
        shapes.add(new Circle());
        shapes.add(new Shape1());
        shapes.add(new Shape2());
        ShapeVisitor visitor = new VolumeShapeVisitor();
        System.out.println(calculateShapeAttributes(visitor, shapes));
    }

    public static int calculateShapeAttributes(ShapeVisitor visitor, List<Shape> shapes) {
        int result = 0;
        for(Shape shape : shapes) {
            result += shape.accept(visitor);
        }
        return result;
    }
}

I am not gonna comment on why we name things like Visitor and visit, because to be honest I haven't thought of a reasonable explanation. But nevertheless, our derivation of this pattern so far has not touched upon such concepts, so I don't think these are necessary.

Discussion (0)

pic
Editor guide