介绍
访客 [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
等等
访客模式的缺点
-
visit()
方法的返回类型必须在设计时定义。 实际上,在大多数情况下,这些方法是void
。 -
accept()
方法的实现在所有类中都是相同的。 显然,我们更喜欢避免代码重复。 - 每次添加新的模型类时,每个
visitor
必须更新,因此维护变得很困难。 - 对于某些
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
模式所需的某些样板代码。 以下是上述改进的简要清单。
- 这里描述的
Visitor
visit()
方法可以返回值,因此可以实现为纯函数[3],该函数有助于将Visitor模式与功能编程范例结合起来。 - 将整体
Visitor
接口拆分为单独的块可以使其更加灵活,并简化了代码维护。 - 可以在运行时使用构建器来配置
MapVisitor
,因此它可能会更改其行为,具体取决于仅在运行时已知且在开发过程中不可用的信息。 - 具有不同返回类型的访问者可以应用于相同的
Visitable
类。 - 在接口中完成的方法的默认实现会删除许多典型的
Visitor
实现常用的样板代码。
参考文献
- 维基百科
- 区域
- 纯函数的定义 。
翻译自: https://www.javacodegeeks.com/2019/03/new-life-old-visitor-design-pattern.html