"Visiting" a Trivial Java Problem

Some days ago I came across a classic problem, that has a well established solution in the java community.
I will simplify and omit all the details about the specific problem and classes. For the sake of simplicity let’s consider the following scenario.
We have an interface, Food and two implementing classes, Pizza and Pasta:

interface Food {
    String cook();
}

final class Pizza implements Food {
    public String cook() {
        return "Pizza is cooking";
    }
}

final class Pasta implements Food {
    public String cook() {
        return "Pasta is cooking";
    }
}

And we have a List<Food> foods that we want to iterate. During the iteration, we want to have a different behavior depending on whether the element of the list is an instance of Pasta or Pizza. We will simplify this different behavior by printing a prefix before the cook() phrase to System.out, like this:

Pizza: Pizza is cooking

in case of Pizza instance, or:

Pasta: Pasta is cooking

if the element is a Pasta instance.

A quick and simple solution would be to use the instanceof operator, as shown below:

for (Food f: foods) {
  if (f instanceof Pizza) {
    System.out.println("Pizza: " + f.cook());
  } else if (f instanceof Pasta) {
    System.out.println("Pasta: " + f.cook());
  }
}

This seems to solve the problem, but what if the behavior to be implemented becomes more complicated? Or if we anticipate that it will become more complicated? How would it affect the code’s readability? There is an ongoing debate about using the instanceof operator, and I won’t delve into it. If you search the internet, you’ll discover that there are people ready to don armor and engage in battle over this topic. If you happen to encounter such individuals on your team… well, let’s just say it’s not an ideal situation to be in.

Nevertheless, someone might suggest, “Hey man, you can simply put the prefix in the cook method’s behavior, and you’re done!” And they would be right! However, as I mentioned earlier, I am oversimplifying the behavior. Imagine a more complex scenario where you might need another mind-blowing Java construct: the explicit cast.

Because of explicit casts, many people have met their demise, and the last three decades have witnessed some truly bloody battles regarding its usage. I implore you, please don’t risk your life!

So, another approach to solving the problem would be to implement the Visitor pattern. You can find countless explanations about this pattern on the internet, so I will provide the solution directly. We will have a Visitor class that implements methods to handle different instances. As the Visitor pattern prescribes, we need to add an accept method in our object classes. I will explain why in a minute. Let’s see what happens to our code.

interface Food {
    String cook();
    void accept(Visitor v);
}

final class Pizza implements Food {
    public String cook() {
        return "Pizza is cooking";
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}

final class Pasta implements Food {
    public String cook() {
        return "Pasta is cooking";
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}

class Visitor {
    public void visit(Pizza food){
        System.out.println("Pizza: " + food.cook());
    }
    public void visit(Pasta food){
        System.out.println("Pasta: " + food.cook());
    }
}

It’s a simplified version of the Visitor pattern, the full implementation requires the definition of the interface Visitor and its implementation, but It works for our purposes.

To visit the list you would do:

Visitor v = new Visitor();
 for (Food f: foods) {
    f.accept(v);
}

Why we have to implement the accept method and we don’t simply call the visit method like this?

Visitor v = new Visitor();
for (Food f: foods) {
    v.visit(f);
}

The answer is quite easy: Java makes a static bind when you call an overloaded method. If you write the last code, you will have to implement a visit(Food f) method, and Java will always call that method. This way, you are not doing what we want to do unless you use instanceof inside the visit(Food f) method, which has devastating consequences as we discussed before.

Regarding the pros and cons of the Visitor pattern, there are numerous discussions all over the internet. I can say that the pattern is useful in many cases, but sometimes it can become very tedious.

Is that all? Well, it seems that not everyone shares this opinion on the internet. I read this post by Nicolai Parlog some time ago, where he explains that with the new features of Java, the Visitor Pattern becomes useless. I’m referring to JEP-406 better known as Pattern Matching for switch. This feature is a preview, so it has to be enabled with the --enable-preview in the compiler and allows to do something like:

for (Food f: foods) {
    switch (f) {
        case Pizza pizza -> v.visit(pizza);
        case Pasta pasta -> v.visit(pasta);
        default -> System.out.println("unimplemented"); //You can remove this implementing the sealed interface
    }
}

In this case we don’t need the accept method anymore, and, if we want to avoid writing the default option code, we have to implement the Food interface as sealed, like the following:

sealed interface Food
    permits Pizza, Pasta {
    String cook();
    void accept(Visitor v);
}

Is the Visitor pattern really dead in Java? I don’t know. What I do know is that it’s always fascinating to take simple problems like the one proposed and explore possible solutions.

I hope you enjoyed this post, if you want to share it to your contacts on the social media, you can do it by clicking on the social icons at the beginning of this post, otherwise you can copy the url and share it wherever you like.

If you want to reach me, you will find all my contacts in the About page. Sooner or later I will enable comments on my posts, in the meantime, if you have questions, feel free to contact me, I’ll be happy to answer.