Does Java 8 provide an alternative to the visitor pattern?

后端 未结 3 1873
栀梦
栀梦 2021-02-07 14:22

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

Object-oriented

相关标签:
3条回答
  • 2021-02-07 14:27

    Lambda expressions can make it easier to set up (very) poor man's pattern matching. Same technique can be used to make a Visitor easier to build.

    static interface Animal {
        // can also make it a default method 
        // to avoid having to pass animal as an explicit parameter
        static void match(
                Animal animal,
                Consumer<Dog> dogAction,
                Consumer<Cat> catAction,
                Consumer<Fish> fishAction,
                Consumer<Bird> birdAction
        ) {
            if (animal instanceof Cat) {
                catAction.accept((Cat) animal);
            } else if (animal instanceof Dog) {
                dogAction.accept((Dog) animal);
            } else if (animal instanceof Fish) {
                fishAction.accept((Fish) animal);
            } else if (animal instanceof Bird) {
                birdAction.accept((Bird) animal);
            } else {
                throw new AssertionError(animal.getClass());
            }
        }
    }
    
    static void jump(Animal animal) {
        Animal.match(animal,
                Dog::hop,
                Cat::leap,
                fish -> {
                    if (fish.canJump()) {
                        fish.jump();
                    } else {
                        fish.swim();
                    }
                },
                Bird::soar
        );
    }
    
    0 讨论(0)
  • 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.

    0 讨论(0)
  • 2021-02-07 14:39

    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.

    0 讨论(0)
提交回复
热议问题