使用Java新的模式切换替代访问者模式 - nipafx


在现代 Java 中,不再需要访问者模式。使用现代 Java 语言功能的模式匹配的密封(Sealed)类型和切换((Pattern Switches))可以用更少的代码和更少的复杂性实现相同的目标。
访问者设计模式是一种将算法与其操作的对象结构分离的方法。这种分离的一个实际结果是能够在不修改结构的情况下向现有对象结构添加新操作。
不修改结构是这里的关键动机。使用访问者模式,每个操作都在访问者中实现,然后将其传递给对象结构,对象结构将其构成对象传递给访问者。该结构不知道任何特定访问者,因此可以在需要操作的任何地方自由创建它们。

public class VisitorDemo {

    public static void main(final String[] args) {
        Car car = new Car();
        car.accept(new CarElementPrintVisitor());
    }

}

// supertype of all objects in the structure
interface CarElement {

    void accept(CarElementVisitor visitor);

}

// supertype of all operations
interface CarElementVisitor {

    void visit(Body body);
    void visit(Car car);
    void visit(Engine engine);

}

class Body implements CarElement {

  @Override
  public void accept(CarElementVisitor visitor) {
      visitor.visit(this);
  }

}

class Engine implements CarElement {

  @Override
  public void accept(CarElementVisitor visitor) {
      visitor.visit(this);
  }

}

class Car implements CarElement {

    private final List<CarElement> elements;

    public Car() {
        this.elements = List.of(new Body(), new Engine());
    }

    @Override
    public void accept(CarElementVisitor visitor) {
        for (CarElement element : elements) {
            element.accept(visitor);
        }
        visitor.visit(this);
    }

}

class CarElementPrintVisitor implements CarElementVisitor {

    @Override
    public void visit(Body body) {
        System.out.println(
"Visiting body");
    }

    @Override
    public void visit(Car car) {
        System.out.println(
"Visiting car");
    }

    @Override
    public void visit(Engine engine) {
        System.out.println(
"Visiting engine");
    }

}

现代 Java 提供了比模式更好的方法来实现访问者模式的目标,并使其变得多余。 

Java新语法实现
我们现在可以以更简单的方式实现这些目标:

  • 为属于这些操作的所有类型创建一个sealed接口
  • 任何需要一个新的操作,使用类型图案 中switch实现它(这是一个预览功能,在Java17中)
  • 密封接口 switch ,和模式匹配

sealed interface CarElement
    permits Body, Engine, Car { }

final class Body implements CarElement { }

final class Engine implements CarElement { }

final class Car implements CarElement {

    // ...

}

// elsewhere, wherever you have a `CarElement`:
// one piece of code per operation - this one prints stuff
String message = switch (element) {
    case Body body ->
"Visiting body";
    case Car car ->
"Visiting car";
    case Engine engine ->
"Visiting engine";
    
// note lacking `default` branch - that's important!
};
System.out.println(message);

要点

  1. switch(element)基于element 切换
  2. 每个case标签测试该实例是否属于指定类型:如果是,它会创建一个具有新名称的该类型的变量(在我的示例中未使用),然后 switch 评估为箭头右侧的字符串
  3. switch必须评估一个结果,然后分配给 message

“必须评估结果”部分在没有default switch的情况下工作,因为CarElement是密封的,这让编译器(和您的同事)知道只有列出的类型直接实现它。编译器可以将该知识应用于模式切换并确定列出的情况是详尽无遗的,即检查所有可能的实现。
所以当你在密封接口中添加一个新类型时,所有没有default switch的模式切换会突然变得不穷尽并导致编译错误。就像向visit访问者界面添加新方法时一样,这些编译错误很好- 它们会将您带到需要更改代码以处理新情况的地方。因此,您可能不应该default向此类开关添加switch - 如果您想将某些类型转换为无操作,请明确列出它们:
String message = switch (element) {
    case Body body -> // do a thing
    case Car car ->
// do the default thing
    case Engine engine ->
// do the (same) default thing
};

 
重用迭代逻辑
访问者模式实现了内部迭代。这意味着不是数据结构的每个用户都实现自己的迭代(在数据结构之外的用户代码中,因此是external),而是将要执行的操作交给数据结构,然后数据结构对其自身进行迭代(此代码在内部结构,因此内部)并应用动作:
class Car implements CarElement {

    private final List<CarElement> elements;

    // ...

    @Override
    public void accept(CarElementVisitor visitor) {
        for (CarElement element : elements) {
            element.accept(visitor);
        }
        visitor.visit(this);
    }

}

// elsewhere
Car car =
// ...
CarElementVisitor visitor =
// ...
car.accept(visitor);

这有重用迭代逻辑的好处,如果它不像直接循环那样微不足道,那就特别有趣。缺点是很难涵盖迭代的许多用例:查找结果,计算新值并将它们收集在列表中,将值减少到单个结果等。我想你明白这是怎么回事:Java 流已经做所有这些以及更多!因此,与其实施 的临时变体,为什么不使用真正的交易呢?Stream::forEach

final class Car implements CarElement {

    private final List<CarElement> elements;

    // ...

    public Stream<CarElement> elements() {
        return Stream.concat(elements.stream(), Stream.of(this));
    }

}

// elsewhere
Car car =
// ...
car.elements()
    
// do stream things

 
现代 Java 解决方案
这是完整的解决方案:

public class VisitorDemo {

    public static void main(final String[] args) {
        Car car = new Car();
        print(car);
    }

    private static void print(Car car) {
        car.elements()
            .map(element -> switch (element) {
                case Body body -> "Visiting body";
                case Car car_ ->
"Visiting car";
                case Engine engine ->
"Visiting engine";
            })
            .forEach(System.out::println);
    }

}

// supertype of all objects in the structure
sealed interface CarElement
        permits Body, Engine, Car { }

class Body implements CarElement { }

class Engine implements CarElement { }

class Car implements CarElement {

    private final List<CarElement> elements;

    public Car() {
        this.elements = List.of(new Body(), new Engine());
    }

    public Stream<CarElement> elements() {
        return Stream.concat(elements.stream(), Stream.of(this));
    }

}

相同的功能,但代码行数减少了一半,而且没有间接引用。还不错吧?
总结:

  • 为结构的类型创建接口 sealed
  • 对于操作,使用模式切换(pattern switches)来确定每种类型的代码路径
  • switch中避免使用default,这样可在添加新类型时避免在每个操作中出现编译错误