翻译 | 访问者模式的新生

2019-03-312508

原文自国外java社区javacodegeeks,作者为 Alexander Radzin,传送门

简介

访问者模式是一个广为人知的经典的设计模式。有许多文章都有关于它的详细的介绍。在不对它的深入研究下,我将简要地回顾这个模式的概念,解释它的优点和缺点,并且提出一些能够在 Java 中轻松地应用这个模式的一些改进点。

典型的访问者模式(Vistor)

[Vistor] 允许在运行时对一组对象进行一个或者多个的操作,从而将操作与对象结构间的解耦。(来自 Gang of Four book 的解释)

这个模式基于接口调用。Visitable 必须由模型类及一系列实现每个关联的模型类的方法(算法)的 Visitors 来实现。

public interface Visitable {
  public void accept(Visitor visitor);
}

public class Book implements Visitable {
   .......
   @Override public void accept(Visitor visitor) {visitor.visit(this)};
   .......
}

public class Cd implements Visitable {
   .......
   @Override public void accept(Visitor visitor) {visitor.visit(this)};
   .......
}

interface Visitor {
   public void visit(Book book);
   public void visit(Magazine magazine);
   public void visit(Cd cd);
}

现在我们实现各种各样的 visitors,例如:

  • 对打印操作提供 VisitablePrintVisitor
  • DbVisitor 将数据存储进数据库
  • ShoppingCart 将数据添加进行购物车

等等。

观察者模式的缺点

  1. visit() 方法的返回类型必须在设计的时候定义。事实上,在大多情况下返回都是 void
  2. accept() 方法的实现在所有类中都是相同的。显然,我们更希望避免代码上的重复
  3. 每次新模型类添加的时候,每一个 vistor 都必须更新,所以维护起来变得非常困难
  4. 在确定的 vistor 中确定的模型类当中设置可选的实现是不可能的。例如,当牛奶不能被寄送时,程序能够给购买者发送一封邮件。在这当中,这两种方式都能通过传统的邮寄方式投递。所以,EmailSendingVisitor 不能实现方法 visit(Milk),但可以实现 visit(Software)。可能的解决方法是抛出 UnsupportedOperationException 但调用者不能在方法调用之前知道这个异常会被抛出

典型的访问者模式的改进

返回值

首先,我们对 Vistor 接口添加返回值。使用泛型来完成一般的定义。

public interface Visitable {
  public <R> R accept(Visitor<R> visitor);
}


interface Visitor<R> {
   public R visit(Book book);
   public R visit(Magazine magazine);
   public R visit(Cd cd);
}

很好,这看起来十分简单。现在我们能够将任何返回值的 Visitor 应用到 book 当中了。例如, DbVisitor 可以返回数据库中修改的记录数目(整型),ToJson 访问者可以返回 对象的 JSON 表达形式的字符串。(可能这个例子不是太典型,在现实的编码中,我们通常使用其他技术去实现 JSON 的对象序列化,但从理论上来说,作为访问者模式的可能用法,它已经足够好用了。)

默认实现

接着,让我们感谢 Java8 提供实现,使得在接口内可以保存默认实现:

public interface Visitable<R> {
  default R accept(Visitor<R> visitor) {
      return visitor.visit(this);
  }
}

现在,实现 Visitable 的类不必自己实现 visit() 方法了:默认实现在大多数情况下都适用。

上面的改进解决了缺点1和2。

MonoVisitor

让我们尝试进一步的改进。首先,定义下面这样的 MonoVisitor 接口:

public interface MonoVisitor<T, R> {
    R visit(T t);
}

为了避免命名重复和可能存在的混淆,这里将名称 Visitor 更改为 MonoVisitor。通过 book 的 visitor 定义许多重载方法 visit(),每一个都可以接收对应的 Visitable 中不同类型的参数。因此,Visitor 的定义是不能通用的,它必须在项目级别上被定义和维护。MonoVisitor 只定义了一个单一方法,通过泛型保证了类型的安全性。即使使用不同的泛型参数,单个类也无法多次实现相同接口。这代表即使将 MonoVisitor 划分到同一个类中,我们也必须做到多个单独的实现。

使用函数引用而非 Visitor

由于 MonoVistor 只有一个业务方法,我们必须对每个模型类提供实现。然而,我们不希望创建单独的顶级类,而想将它们分组到同一个类中。新的 visitor 持有各种可访问类以及 java.util.Function 的实现的映射 map,并且将 visit() 方法的调用分发给特定的实现当中。

现在来看下 MapVisitor。

public class MapVisitor<R> implements
        Function<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> {
    private final Map<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> visitors;

    MapVisitor(Map<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> visitors) {
        this.visitors = visitors;
    }

    @Override
    public MonoVisitor apply(Class clazz) {
        return visitors.get(clazz);
    }
}

MapVisitor

  • 为了检索特定的实现,实现 Function
  • 在 map 中接收类和实现的映射
  • 检索适合于给定类的特定实现

MapVisitor 带有一个 package-private 的构造函数。使用特定构造器完成的 MapVisitor 的初始化是十分简单和灵活的:

MapVisitor<Void> printVisitor = MapVisitor.builder(Void.class)
        .with(Book.class, book -> {System.out.println(book.getTitle()); return null;})
        .with(Magazine.class, magazine -> {System.out.println(magazine.getName()); return null;})
        .build();

MapVisitor 和其中一个传统的 Visitor 在用法上很相似:

someBook.accept(printVisitor);
someMagazine.accept(printVisitor);

MapVisitor 还有一个好处。所有定义在传统的 visitor 中的接口方法必须被实现。然而,通常有些方法是无法被实现的。

例如,我们希望实现一个应用程序来演示动物可以做的各种动作。用户可以挑选一种动物并且通过在菜单中选择特定的动作来使它去完成。

这里是动物的列表:Duck, Penguin, Wale, Ostrich

这里是动作的类型:Walk, Fly, Swim

我们决定对每一个动作都提供一个 visitor:WalkVisitor, FlyVisitor, SwimVisitor。duck 能够完成所有三项动作,Penguin 不能飞,Wale 只能游泳,Ostrich 只能行走。所以,我们决定抛出异常如果用户想让 Wale 行走或者想让 Ostrich 飞翔。但是这种行为是对用户不友好的。事实上,用户只有在按下动作按钮时才会得到错误信息。我们更希望去禁用不相关的按钮。MapVisitor 在不添加额外的数据结构或者代码复用就能够做到,更甚我们也不需要定义新的或者扩展任何其他接口。相反。我们更喜欢使用标准接口 java.util.Predicate

public class MapVisitor<R> implements
        Function<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>>, 
        Predicate<Class<? extends Visitable>> {
    private final Map<Class<? extends Visitable>, MonoVisitor<? extends Visitable, R>> visitors;
    ...............
    @Override
    public boolean test(Class<? extends Visitable> clazz) {
        return visitors.containsKey(clazz);
    }
}

现在,我们能够调用 test() 方法来定义是否启用或显示所选动物的动作按钮了。

总结

本篇文章演示了几个改进点,使得以往的访问者模式变得更加灵活和强大了。建议的实现避免了典型的访问者模式实现中出现的一些样板代码。以下是以上提到的提升的简短总结:

  1. visit() 方法能够返回值因此可以被当作 pure funcions 来实现,有助于将访问者模式和函数式编程范式融合在一起
  2. 将单一的 Visitor 接口拆分成单独的块,使其更灵活,并简化了代码维护
  3. MapVisitor 可以在运行时使用 builder 进行配置,因此它可以根据只有在运行时已知的信息以及开发中不可用的信息来改变其行为
  4. 具有不同返回类型的访问者可以应用于相同的 Visitable
  5. 接口中默认实现方法移除掉大量的在典型的 Visitor 实现中的样本代码

引用

  1. Wikipedia
  2. DZone
  3. Definition of pure function

译者总结

访问者模式也算是设计模式中比较经典的一种了,它通过访问者,解决了不同访问者调用数据的复用问题。但是也同样出现问题,譬如如果添加新的模型结构造成访问者重新定义问题等等。文章中通过使用泛型和函数式编程等方式,对原有模式进行改进,希望可以给到读者们启发。

分享
点赞2
打赏
上一篇:Docker常用命令笔记(一)
下一篇:翻译 | Java 中的不可变对象