我所理解的访问者模式将对元素访问时进行的操作从元素本身抽离出来,避免了在新增操作时需要修改被访问元素的弊端(开闭原则);同时按照业务逻辑组织访问者类,做到了单一职责原则。
定义
GoF中的定义如下:
意图:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义新操作。
设计模式学习网站 中给出的结构如下
这里的accept方法使用了一种叫做“double-dispatch”的策略,目的是在访问节点时让节点自主选择该使用visitor的哪种方法,而非让visitor去识别。也就是将下面第一种风格的代码改成第二种风格的。
foreach (Node node in graph)if (node instanceof City)exportVisitor.doForCity((City) node)if (node instanceof Industry)exportVisitor.doForIndustry((Industry) node)// ...
}
// Client code
foreach (Node node in graph)node.accept(exportVisitor)// City
class City ismethod accept(Visitor v) isv.doForCity(this)// ...// Industry
class Industry ismethod accept(Visitor v) isv.doForIndustry(this)// ...
可以发现, 第二种风格的代码还是对原有的类有所改动的,所以在一定程度上违反了“开放-封闭”原则。不过这样的改动基本上是一次性的,后续对方法的扩展不会影响到原有的类。
与其他模式的配合
访问者模式常常与迭代器模式配合,实现对某种结构的遍历,如在静态分析领域对抽象语法树(AST)的遍历以及在遍历过程中对信息的收集。
实际案例
antlr
antlr 是一个静态分析工具的生成器,支持任意语言的词法分析、语法分析、抽象语法树生成和遍历(只要提供符合g4标准的文法)。常用于IDE的语法检查、接口文档生成、不同语言代码之间的转译等任务。
antlr 提供了一个扮演迭代器角色的类 ParseTreeWalker
,并且提供了两种访问者模式(Listener和Visitor)。当walker实例遇到语法节点时,就会触发listener或visitor里面实现了的钩子方法(这里为每个节点提供了enter和exit两个钩子)。树的遍历采用了深度优先的策略。典型的用法如下
ParseTree tree = ... ; // tree is result of parsing
MyVisitor v = new MyVisitor();
v.visit(tree);
按照官方文档的说法,antlr中listener和visitor的差别在于visitor可以显式地控制遍历路径。
babel
babel 是一个面向JavaScript语言的转译器,支持不同版本的EcmaScript之间的转换,以及JSX等DSL到JavaScript的转译。
和antlr类似,babel也把抽象语法树作为中间表示,并通过对树的遍历来支持各种转译任务。其中,AST的生成可以使用@babel/parse
包完成,而遍历则可以用@babel/traverse
,目标代码的生成可以使用@babel/generator
。它和访问者模式相关的用法如下:
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";// 源代码
const code = `function square(n) {return n * n;
}`;// 生成抽象语法树
const ast = parser.parse(code);traverse(ast, {enter(path) {// 使用访问者模式在遍历过程中对节点进行修改if (path.isIdentifier({ name: "n" })) {path.node.name = "x";}},
});