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

2022-09-03 13:20:01

Stack Overflow上这个流行的答案是关于函数式编程和面向对象编程之间的区别:

面向对象的语言是好的,当你对事物有一组固定的操作,并且随着你的代码的发展,你主要是添加新的东西。这可以通过添加实现现有方法的新类来实现,并且现有类将单独保留。

当你有一组固定的东西时,函数式语言是很好的,随着你的代码的发展,你主要是在现有的东西上添加新的操作。这可以通过添加使用现有数据类型进行计算的新功能来实现,并且现有功能将单独保留。

假设我有一个接口:Animal

public interface Animal {
    public void speak();
}

我有一个 、 、 和 所有实现接口。如果我想添加一个新方法到 named ,我必须遍历我所有的子类并实现 。DogCatFishBirdAnimaljump()jump()

访问者模式可以缓解这个问题,但是似乎通过Java 8中引入的新功能特性,我们应该能够以不同的方式解决这个问题。在我可以轻松地使用模式匹配,但Java还没有真正做到这一点。scala

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


答案 1

你试图完成的事情虽然令人钦佩,但在大多数情况下并不适合Java。但在我开始之前...

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

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

但最终,您将不得不为每种类型的功能提供功能。我没有看到添加新方法和创建访问者类或部分函数之间的巨大差异。这只是一个位置问题。是否要按操作或对象组织代码?(函数式或面向对象,动词或名词等)

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

访客模式和静态方法可能是按操作组织事物的最佳选择。但是,我认为当访问者并不真正依赖于他们正在访问的对象的确切类型时,他们最有意义。例如,动物访客可能被用来让动物说话,然后跳跃,因为这两件事都得到了所有动物的支持。跳跃访客对我来说没有多大意义,因为这种行为本质上是针对每种动物的。

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

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

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为你设计的工作。

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

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

附言因为我是那个家伙,™所以我要提出Lisp,特别是Common Lisp Object System(CLOS)。它提供了基于所有参数进行调度的多方法。《Practical Common Lisp》一书甚至提供了一个例子,说明它与Java有何不同


答案 2

对Java语言的添加并没有使每个旧概念都过时。实际上,访客模式非常擅长支持添加新操作。

当将此模式与新的 Java 8 可能性进行比较时,以下几点变得显而易见:

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

因此,新的Java 8功能永远无法替代访问者模式,但是,寻找可能的协同效应是合理的。此答案讨论了改造现有 API () 以启用 lambda 表达式的可能性。该解决方案是一个专门的具体访问者实现,它委托给可以为每个方法指定的相应函数。如果每个函数都是可选的(即每个方法都有一个合理的默认值),如果应用程序只对可能操作的一小部分感兴趣,或者如果它想统一处理其中的大部分,它将派上用场。FileVisitorvisitvisit

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

如果将收集器视为 .它由多达四个函数组成,这是访问扁平,均匀的项目序列所能想象的最大功能。您不必实现该接口,而是可以使用三个函数来启动指定单个函数可变约简的归约,但是在一些常见情况下,指定现有实现比通过 lambda 表达式/方法引用指定所有必需的函数更简洁,例如使用 或 。StreamStreamcollect(Collectors.toList())collect(Collectors.joining(","))

当向访问者模式的特定应用程序添加此类支持时,它将使调用站点更加闪亮,而特定方法的实现站点始终很简单。因此,唯一保持笨重的部分是访客类型本身;当它通过支持基于功能接口的操作进行增强时,它甚至可能变得更加复杂。在不久的将来,不太可能有一个基于语言的解决方案,无论是更简单地创建此类访问者还是替换此概念。accept