旧访客设计模式的新生活

介绍

访客 [1、2]是众所周知的经典设计模式。 有很多资源对其进行了详细说明。 在不深入研究实现的情况下,我将简要提醒一下该模式的概念,解释其优点和缺点,并提出一些可以使用Java编程语言轻松应用于其的改进。

古典游客

[Visitor] 允许在运行时将一个或多个操作应用于一组对象,从而将操作与对象结构分离。

(四人帮)

该模式基于通常称为的接口。 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 )}; 
    .......  }  Visitor { interface Visitor { 
    public void visit(Book book); 
    public void visit(Magazine magazine); 
    public void visit(Cd cd);  } 

现在我们可以实现各种visitors ,例如

  • PrintVisitor是提供打印Visitable
  • DbVisitor其存储在数据库中的DbVisitor
  • 将其添加到购物车的ShoppingCart

等等

访客模式的缺点

  1. visit()方法的返回类型必须在设计时定义。 实际上,在大多数情况下,这些方法是void
  2. accept()方法的实现在所有类中都是相同的。 显然,我们更喜欢避免代码重复。
  3. 每次添加新的模型类时,每个visitor必须更新,因此维护变得很困难。
  4. 对于某些visitor ,某些模型类不可能有可选的实现。 例如,可以通过电子邮件将软件发送给买方,而不能发送牛奶。 但是,两者都可以使用传统的邮寄方式递送。 因此, EmailSendingVisitor不能实现方法visit(Milk)但可以实现visit(Software) 。 可能的解决方案是引发UnsupportedOperationException但调用者无法提前知道在调用该方法之前将引发此异常。

经典访客模式的改进

返回值

首先,让我们将返回值添加到Visitor接口。 通用定义可以使用泛型来完成。

 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应用于我们的图书。 例如, DbVisitor可能返回DB(整数)中已更改记录的数量,而ToJson访问者可能会将我们对象的JSON表示形式返回为String。 (该示例可能不太有机,在现实生活中,我们通常使用其他技术将对象序列化为JSON,但是就其在理论上可以使用Visitor模式而言,已经足够了)。

默认实现

接下来,让我们感谢Java 8在接口内保留默认实现的能力:

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

现在,实现Visitable类本身不必实现>visit() :在大多数情况下,默认实现就足够了。

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

单访客

让我们尝试应用进一步的改进。 首先,让我们定义接口MonoVisitor如下:

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

Visitor名称已更改为MonoVisitor以避免名称冲突和可能的混淆。 通过这本书, visitor定义了许多重载方法visit() 。 他们每个人都为每个Visitable接受不同类型的参数。 因此,根据定义, Visitor不能是通用的。 必须在项目级别上定义和维护它。 MonoVisitor仅定义一种方法。 类型安全由泛型保证。 即使使用不同的通用参数,单个类也无法多次实现相同的接口。 这意味着即使将MonoVisitor多个实现分为一组,我们也必须保留它们。

功能参考,而不是访客

由于MonoVisitor只有一种业务方法,因此我们必须为每个模型类创建实现。 但是,我们不想创建单独的顶级类,而是希望将它们分组为一个类。 这个新的visitor在各种Visitable类与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

    为了检索特定的实现(为便于阅读,此处省略了完整的泛型;有关详细定义,请查看代码段)

  • 接收映射中类和实现之间的映射
  • 检索适合给定类的特定实现

MapVisitor具有程序包专用的构造函数。 使用特殊构建器完成的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还有一个好处。 必须实现在传统访问者的接口中声明的所有方法。 但是,通常无法实现某些方法。

例如,我们想要实现演示动物可以执行的各种动作的应用程序。 用户可以选择一种动物,然后通过从菜单中选择特定的动作来使它做某事。
这是动物名单: Duck, Penguin, Wale, Ostrich 这是动作列表: Walk, Fly, Swim. 我们决定按操作设置访问者: WalkVisitor, FlyVisitor, SwimVisitor 。 鸭子可以做所有三个动作,企鹅不能飞,瓦尔只能游泳, 鸵鸟只能行走。 因此,如果用户试图使Wale走路或Ostrich飞行,我们决定抛出异常。 但是这种行为不是用户友好的。 确实,用户只有在按下操作按钮时才会收到错误消息。 我们可能更希望禁用不相关的按钮。 MapVisitor允许此操作而无需其他数据结构或代码重复。 我们甚至不必定义new或扩展任何其他接口。 相反,我们更喜欢使用标准接口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()来定义是否必须启用或显示选定动物的操作按钮。

github上提供了此处使用的示例的完整源代码。

结论

本文演示了一些改进,这些改进使旧的Visitor模式变得更加灵活和强大。 建议的实现方式避免了实现经典的Vistor模式所需的某些样板代码。 以下是上述改进的简要清单。

  1. 这里描述的Visitor visit()方法可以返回值,因此可以实现为纯函数[3],该函数有助于将Visitor模式与功能编程范例结合起来。
  2. 将整体Visitor接口拆分为单独的块可以使其更加灵活,并简化了代码维护。
  3. 可以在运行时使用构建器来配置MapVisitor ,因此它可能会更改其行为,具体取决于仅在运行时已知且在开发过程中不可用的信息。
  4. 具有不同返回类型的访问者可以应用于相同的Visitable类。
  5. 在接口中完成的方法的默认实现会删除许多典型的Visitor实现常用的样板代码。

参考文献

  1. 维基百科
  2. 区域
  3. 纯函数的定义

翻译自: https://www.javacodegeeks.com/2019/03/new-life-old-visitor-design-pattern.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值