Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Java 8 provide an alternative to the visitor pattern?

This popular answer on Stack Overflow has this to say about the difference between functional programming and object-oriented programming:

Object-oriented languages are good when you have a fixed set of operations on things, and as your code evolves, you primarily add new things. This can be accomplished by adding new classes which implement existing methods, and the existing classes are left alone.

Functional languages are good when you have a fixed set of things, and as your code evolves, you primarily add new operations on existing things. This can be accomplished by adding new functions which compute with existing data types, and the existing functions are left alone.

Say I have an Animal interface:

public interface Animal {
    public void speak();
}

And I have a Dog, Cat, Fish, and Bird that all implement the interface. If I want to add a new method to Animal named jump(), I would have to go through all of my subclasses and implement jump().

The visitor pattern can alleviate this problem, but it seems that with the new functional features introduced in Java 8 we should be able to solve this problem in a different manner. In scala I could easily just use pattern matching, but Java doesn't really have that yet.

Does Java 8 actually make it any easier to add new operations on existing things?

like image 735
Cory Klein Avatar asked Mar 17 '15 02:03

Cory Klein


People also ask

What is visitor pattern in Java?

Visitor is a behavioral design pattern that allows adding new behaviors to existing class hierarchy without altering any existing code.

Why to use visitor pattern?

The purpose of a Visitor pattern is to define a new operation without introducing the modifications to an existing object structure. Imagine that we have a composite object which consists of components.

In which scenario would you use the visitor pattern?

The visitor pattern is used when: Similar operations have to be performed on objects of different types grouped in a structure (a collection or a more complex structure). There are many distinct and unrelated operations needed to be performed.


Video Answer


2 Answers

What you're trying to accomplish, while admirable, isn't a good fit for Java in most cases. But before I get into that...

Java 8 adds default methods to interfaces! You can define default methods based on other methods in the interface. This was already available to abstract classes.

public interface Animal {
    public void speak();
    public default void jump() {
        speak();
        System.out.println("...but higher!");
    }
}

But in the end, you're going to have to provide functionality for each type. I don't see a huge difference between adding a new method and creating a visitor class or partial functions. It's just a question of location. Do you want to organize your code by action or object? (functional or object oriented, verb or noun, etc.)

I suppose the point I'm trying to make is that Java code is organized by 'noun' for reasons that aren't changing any time soon.

The visitor pattern along with static methods are probably your best bet for organizing things by action. However, I think visitors make the most sense when they don't really depend on the exact type of the object they're visiting. For instance, an Animal visitor might be used to make the animal speak and then jump, because both of those things are supported by all animals. A jump visitor doesn't make as much sense to me because that behavior is inherently specific to each animal.

Java makes a true "verb" approach a little difficult because it chooses which overloaded method to run based on the compile time type of the arguments (see below and Overloaded method selection based on the parameter's real type). Methods are only dynamically dispatched based on the type of this. That's one of the reasons inheritance is the preferred method of handling these types of situations.

public class AnimalActions {
    public static void jump(Animal a) {
        a.speak();
        System.out.println("...but higher!");
    }
    public static void jump(Bird b) { ... }
    public static void jump(Cat c) { ... }
    // ...
}
// ...
Animal a = new Cat();
AnimalActions.jump(a); // this will call AnimalActions.jump(Animal)
                       // because the type of `a` is just Animal at
                       // compile time.

You can get around this by using instanceof and other forms of reflection.

public class AnimalActions {
    public static void jump(Animal a) {
        if (a instanceof Bird) {
            Bird b = (Bird)a;
            // ...
        } else if (a instanceof Cat) {
            Cat c = (Cat)a;
            // ...
        }
        // ...
    }
}

But now you're just doing work the JVM was designed to do for you.

Animal a = new Cat();
a.jump(); // jumps as a cat should

Java has a few tools that make adding methods to a broad set of classes easier. Namely abstract classes and default interface methods. Java is focused on dispatching methods based on the object invoking the method. If you want to write flexible and performant Java, I think this is one idiom you have to adopt.

P.S. Because I'm That Guy™ I'm going to bring up Lisp, specifically the Common Lisp Object System (CLOS). It provides multimethods that dispatch based on all arguments. The book Practical Common Lisp even provides an example of how this differs from Java.

like image 129
axblount Avatar answered Oct 21 '22 10:10

axblount


The additions made to the Java language do not render every old concept outdated. In fact, the Visitor pattern is very good at supporting adding of new operations.

When comparing this pattern with the new Java 8 possibilities, the following becomes apparent:

  • Java 8 allows to easily define operations comprising a single function. This comes handy when processing flat homogeneous collections like with Iterable.forEach, Stream.forEach but also Stream.reduce
  • A visitor allows to define a multiple functions which are selected by element type and/or topology of a data structure which becomes interesting right where the single function feature stops working, when processing heterogeneous collections and non-flat structures, e.g. trees of items

So the new Java 8 features can never act as a drop-in replacement for the Visitor pattern, however, searching for possible synergies is reasonable. This answer discusses possibilities to retrofit an existing API (FileVisitor) to enable the use of lambda expressions. The solution is a specialized concrete visitor implementation which delegates to corresponding functions which can be specified for each visit method. If each function is optional (i.e. there is a reasonable default for each visit method), it will come handy if the application is interested in a small subset of the possible actions only or if it wants to treat most of them uniformly.

If some of these use cases are regarded “typical”, there might be an accept method taking one or more functions creating the appropriate delegating visitor behind the scene (when designing new APIs or improving API under your control). I wouldn’t drop the ordinary accept(XyzVisitor), however, as the option to use an existing implementation of a visitor should not be underestimated.

There’s a similar choice of overloads in the Stream API, if we consider a Collector as a kind of visitor for a Stream. It consists of up to four functions, which is the maximum imaginable for visiting a flat, homogeneous sequence of items. Instead of having to implement that interface, you can initiate a reduction specifying a single function or a mutable reduction using three functions but there are common situations where specifying an existing implementation is more concise, like with collect(Collectors.toList()) or collect(Collectors.joining(",")), than specifying all necessary functions via lambda expressions/ method references.

When adding such support to a particular application of the Visitor pattern, it will make the calling site more shiny while the implementation site of the particular accept methods always has been simple. So the only part which remains bulky is the visitor type itself; it may even become a bit more complicated when it is augmented with support for functional interface based operations. It is unlikely that there will be a language-based solution for either, simpler creation of such visitors or replacing this concept, in the near future.

like image 23
Holger Avatar answered Oct 21 '22 10:10

Holger