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

Does Java 8 provide an alternative to the visitor pattern?

这个关于堆栈溢出的流行答案可以说明函数编程和面向对象编程之间的区别:

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.

假设我有一个Animal接口:

1
2
3
public interface Animal {
    public void speak();
}

我有一个DogCatFishBird来实现接口。如果我想在Animal中添加一个名为jump()的新方法,我将不得不遍历所有子类并实现jump()

访问者模式可以减轻这个问题,但是似乎在Java 8中引入了新的功能特性,我们应该能够以不同的方式解决这个问题。在EDCOX1,8中,我可以很容易地使用模式匹配,但是Java还没有真正的模式匹配。

Java 8实际上是否更容易在现有的事情上添加新操作?


在大多数情况下,你想达到的目标是令人钦佩的,但不适合Java。但在我开始之前…

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

1
2
3
4
5
6
7
public interface Animal {
    public void speak();
    public default void jump() {
        speak();
        System.out.println("...but higher!");
    }
}

但最后,您必须为每种类型提供功能。我看不出在添加新方法和创建访问者类或部分函数之间有什么巨大的区别。只是位置问题。是否按操作或对象组织代码?(功能或面向对象、动词或名词等)

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

访问者模式和静态方法可能是按操作组织事物的最佳选择。然而,我认为当游客不真正依赖他们所参观的对象的确切类型时,他们是最有意义的。例如,一个动物访客可能被用来让动物说话,然后跳,因为这两种东西都是由所有动物支持的。跳跃游客对我来说没什么意义,因为这种行为对每只动物都是固有的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

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

1
2
3
4
5
6
7
8
9
10
11
12
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;
            // ...
        }
        // ...
    }
}

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

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

Java有一些工具可以使方法更容易地添加到一组广泛的类中。即抽象类和默认接口方法。Java专注于基于调用该方法的对象的调度方法。如果你想编写灵活和性能好的Java,我认为这是你必须采用的一个成语。

因为我就是那个人?我将介绍Lisp,特别是通用Lisp对象系统(CLOS)。它提供了基于所有参数进行调度的多方法。这本书实用的通用LISP甚至提供了一个例子,说明了它与Java的区别。


对Java语言的添加不会使每个旧概念过时。实际上,访问者模式非常擅长支持添加新操作。

当将这种模式与新的Java和8种可能性进行比较时,下面变得显而易见:

  • Java&Nbsp 8允许容易地定义包括单个函数的操作。这在处理平面齐次集合时非常方便,例如使用Iterable.forEachStream.forEach以及Stream.reduce
  • 访问者允许定义一个由数据结构的元素类型和/或拓扑选择的多个函数,当处理异类集合和非平面结构(例如项目树)时,在单个函数功能停止工作的地方,它变得有趣。

因此,新的Java&Nbsp 8功能不能作为访问者模式替换的下降,然而,寻找可能的协同作用是合理的。此答案讨论了对现有API(FileVisitor进行改造以启用lambda表达式的可能性。该解决方案是一个专门的具体访问者实现,它委托给可以为每个visit方法指定的相应功能。如果每个函数都是可选的(即每个visit方法都有一个合理的默认值),那么如果应用程序只对可能操作的一小部分感兴趣,或者希望统一处理其中的大部分,那么它将非常有用。

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

如果我们将Collector视为Stream的访问者,那么在StreamAPI中也有类似的过载选择。它由四个功能组成,这是访问一个扁平的、同质的项目序列所能想到的最大值。不必实现该接口,您可以使用三个函数启动一个reducation,指定单个函数或可变reducation,但在某些情况下,指定现有的实现比通过lambda表达式指定所有必需的函数更简洁,例如使用collect(Collectors.toList())collect(Collectors.joining(","))。/方法引用。

当对访问者模式的特定应用程序添加这样的支持时,它将使调用站点更加闪亮,而特定accept方法的实现站点总是很简单。因此,唯一保持庞大的部分是访问者类型本身;当它被扩展为支持基于功能接口的操作时,它甚至可能变得更加复杂。在不久的将来,不太可能会有一个基于语言的解决方案来更简单地创建这样的访问者或取代这个概念。


lambda表达式可以更容易地建立(非常)差的人的模式匹配。同样的技术也可以用来让访问者更容易构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
    );
}