Java 8是否提供了访问者模式的替代方案?

时间:2022-02-27 22:04:38

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

关于Stack Overflow的这个流行的回答可以说明函数式编程和面向对象编程之间的区别:

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().

我有一只狗、猫、鱼和鸟,它们都实现了接口。如果我想为动物添加一个名为jump()的新方法,我必须遍历所有子类并实现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.

访问者模式可以缓解这个问题,但是通过Java 8中引入的新功能特性,我们应该能够以不同的方式解决这个问题。在scala中,我可以很容易地使用模式匹配,但是Java还没有。

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

Java 8是否真的使得在现有的东西上添加新的操作变得更容易了?

3 个解决方案

#1


8  

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。但在我开始之前……

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.

Java 8向接口添加了默认方法!您可以基于接口中的其他方法定义默认方法。抽象类已经可以使用它了。

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.)

但最终,您必须为每种类型提供功能。我不认为添加新方法与创建visitor类或部分函数之间有很大的区别。这只是位置的问题。您希望通过操作或对象来组织代码吗?(面向功能或对象、动词或名词等)

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.

我想我要说的是,Java代码是由名词组织的,其原因不会很快改变。

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.

Java使一个真正的“verb”方法变得有点困难,因为它选择了根据参数的编译时类型来运行的重载方法(参见下面的重载方法选择,基于参数的实际类型)。方法仅根据该类型进行动态调度。这就是继承是处理此类情况的首选方法的原因之一。

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.

您可以通过使用instanceof和其他形式的反射来绕过这个问题。

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.

但现在你只是在做JVM设计为你做的工作。

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.

Java有一些工具,可以使广泛的类的添加方法更加容易。即抽象类和默认接口方法。Java关注的是基于调用该方法的对象的调度方法。如果您想编写灵活且高性能的Java,我认为这是您必须采用的一个习惯用法。

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.

注:因为我那个家伙™我要抚养Lisp,特别是Common Lisp对象系统(clo)。它提供了基于所有参数的多方法。《实用的通用Lisp》甚至提供了一个与Java不同的例子。

#2


6  

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.

添加到Java语言中的内容不会使所有旧的概念都过时。实际上,访问者模式非常擅长支持添加新操作。

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

当将此模式与新的Java 8可能性进行比较时,以下内容变得明显:

  • 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
  • Java 8允许轻松定义包含单个函数的操作。这在处理平面齐次集合时很有用,比如使用Iterable。forEach,流。forEach还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.

因此,新的Java 8特性永远不能替代访问者模式,但是,寻找可能的协作是合理的。这个答案讨论了改造现有API (FileVisitor)以启用lambda表达式的可能性。解决方案是一种专门的具体访问者实现,它代表每个访问方法可以指定的相应函数。如果每个函数都是可选的(即每个访问方法都有一个合理的默认值),如果应用程序只对可能的操作的一小部分感兴趣,或者希望对大多数操作都保持一致,那么它就会派上用场。

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.

如果其中一些用例被认为是“典型的”,那么可能会有一个接受方法,使用一个或多个函数在场景后面创建适当的委托访问者(当设计新的API或在您的控制下改进API时)。但是,我不会放弃普通的accept(XyzVisitor),因为使用访问者现有实现的选项不应该被低估。

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.

流API中也有类似的重载选择,如果我们将收集器视为流的访问者。它由多达四个功能组成,这是参观一个平面的,同质的项目序列所能想到的最大的功能。而不是实现这个接口,您可以启动一个减少指定一个函数或一个可变减少使用三个函数但有共同指定一个现有的情况下实现更简洁,像收集(Collectors.toList())或收集(Collectors.joining(",")),比通过lambda表达式/方法引用指定所有必需的功能。

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.

当向访问者模式的特定应用程序添加此类支持时,它将使调用站点更加闪亮,而特定accept方法的实现站点始终是简单的。所以唯一保持笨重的部分是访问者类型本身;当支持基于功能接口的操作时,它甚至可能变得更加复杂。在不久的将来,不太可能出现基于语言的解决方案来简化此类访问者的创建,或者取代这一概念。

#3


3  

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.

Lambda表达式可以更容易地设置(非常)可怜的人的模式匹配。同样的技术可以用来使访问者更容易构建。

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
    );
}

#1


8  

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。但在我开始之前……

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.

Java 8向接口添加了默认方法!您可以基于接口中的其他方法定义默认方法。抽象类已经可以使用它了。

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.)

但最终,您必须为每种类型提供功能。我不认为添加新方法与创建visitor类或部分函数之间有很大的区别。这只是位置的问题。您希望通过操作或对象来组织代码吗?(面向功能或对象、动词或名词等)

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.

我想我要说的是,Java代码是由名词组织的,其原因不会很快改变。

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.

Java使一个真正的“verb”方法变得有点困难,因为它选择了根据参数的编译时类型来运行的重载方法(参见下面的重载方法选择,基于参数的实际类型)。方法仅根据该类型进行动态调度。这就是继承是处理此类情况的首选方法的原因之一。

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.

您可以通过使用instanceof和其他形式的反射来绕过这个问题。

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.

但现在你只是在做JVM设计为你做的工作。

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.

Java有一些工具,可以使广泛的类的添加方法更加容易。即抽象类和默认接口方法。Java关注的是基于调用该方法的对象的调度方法。如果您想编写灵活且高性能的Java,我认为这是您必须采用的一个习惯用法。

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.

注:因为我那个家伙™我要抚养Lisp,特别是Common Lisp对象系统(clo)。它提供了基于所有参数的多方法。《实用的通用Lisp》甚至提供了一个与Java不同的例子。

#2


6  

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.

添加到Java语言中的内容不会使所有旧的概念都过时。实际上,访问者模式非常擅长支持添加新操作。

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

当将此模式与新的Java 8可能性进行比较时,以下内容变得明显:

  • 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
  • Java 8允许轻松定义包含单个函数的操作。这在处理平面齐次集合时很有用,比如使用Iterable。forEach,流。forEach还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.

因此,新的Java 8特性永远不能替代访问者模式,但是,寻找可能的协作是合理的。这个答案讨论了改造现有API (FileVisitor)以启用lambda表达式的可能性。解决方案是一种专门的具体访问者实现,它代表每个访问方法可以指定的相应函数。如果每个函数都是可选的(即每个访问方法都有一个合理的默认值),如果应用程序只对可能的操作的一小部分感兴趣,或者希望对大多数操作都保持一致,那么它就会派上用场。

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.

如果其中一些用例被认为是“典型的”,那么可能会有一个接受方法,使用一个或多个函数在场景后面创建适当的委托访问者(当设计新的API或在您的控制下改进API时)。但是,我不会放弃普通的accept(XyzVisitor),因为使用访问者现有实现的选项不应该被低估。

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.

流API中也有类似的重载选择,如果我们将收集器视为流的访问者。它由多达四个功能组成,这是参观一个平面的,同质的项目序列所能想到的最大的功能。而不是实现这个接口,您可以启动一个减少指定一个函数或一个可变减少使用三个函数但有共同指定一个现有的情况下实现更简洁,像收集(Collectors.toList())或收集(Collectors.joining(",")),比通过lambda表达式/方法引用指定所有必需的功能。

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.

当向访问者模式的特定应用程序添加此类支持时,它将使调用站点更加闪亮,而特定accept方法的实现站点始终是简单的。所以唯一保持笨重的部分是访问者类型本身;当支持基于功能接口的操作时,它甚至可能变得更加复杂。在不久的将来,不太可能出现基于语言的解决方案来简化此类访问者的创建,或者取代这一概念。

#3


3  

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.

Lambda表达式可以更容易地设置(非常)可怜的人的模式匹配。同样的技术可以用来使访问者更容易构建。

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
    );
}