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

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

当你对事物进行固定的操作时,面向对象的语言是很好的,随着代码的发展,你主要添加新的东西。 这可以通过添加实现现有方法的新类来完成,并且现有类保持不变。

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

说我有一个Animal界面:

 public interface Animal { public void speak(); } 

我有一只DogCatFishBird都可以实现界面。 如果我想向Animal命名为jump()添加一个新方法,我将不得不浏览所有子类并实现jump()

访问者模式可以缓解这个问题,但似乎随着Java 8中引入的新function特性,我们应该能够以不同的方式解决这个问题。 在scala我可以很容易地使用模式匹配,但Java还没有真正拥有它。

Java 8实际上是否更容易在现有的东西上添加新操作

在大多数情况下,你想要实现的目标虽然令人钦佩,但并不适合Java。 但在我进入之前……

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

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

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

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

访问者模式以及静态方法可能是您按行动组织事物的最佳选择。 但是,我认为访客最有意义的是他们并不真正依赖于他们访问的对象的确切类型。 例如,动物访客可能会被用来使动物说话然后跳跃,因为所有动物都支持这些动物。 跳跃的访客对我来说并没有多大意义,因为这种行为本质上对每只动物都是特定的。

Java使得真正的“动词”方法有点困难,因为它根据参数的编译时类型选择运行哪个重载方法(参见下文和基于参数实际类型的重载方法选择 )。 方法仅根据其类型动态调度。 这是inheritance是处理这些类型情况的首选方法的原因之一。

 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和其他forms的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; // ... } // ... } } 

但是现在你正在做JVM专为你做的工作。

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

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

PS因为我是That Guy ™我将提出Lisp,特别是Common Lisp对象系统(CLOS)。 它提供了基于所有参数进行调度的多方法。 Practical Common Lisp一书甚至提供了一个与Java有何不同的例子 。

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

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

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

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

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

如果我们将Collector视为Stream的一种访问者,则在Stream API中有类似的重载选择。 它由最多四个函数组成,这是访问平坦,均匀的项目序列所能想到的最大值。 您可以使用三个函数启动指定单个函数或可变约简的减少 ,而不是必须实现该接口,但是通常情况下指定现有实现更简洁,例如collect(Collectors.toList())collect(Collectors.joining(",")) ,而不是通过lambda表达式/方法引用指定所有必需的函数。

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

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 dogAction, Consumer catAction, Consumer fishAction, Consumer 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 ); }