使用访问者模式给抽象语法树整活
在上一篇文章中,为了给访问者模式做铺垫,我从零开始撸一个抽象语法树出来,今天我们用访问者模式给语法树整点活。
访问者模式
访问者模式是一种行为设计模式,它将算法与其所作用的对象隔离开来。通过这种方式,元素的执行算法可以随着访问者的改变而改变。对于Java而言,访问者模式为我们提供了双重分派的功能。
需求
访问者模式的概念说起来有点绕,我们先看看这次改造的需求。既然我们已经实现了语法树,那么有两个后续的需求:
- 打印当前表达式的逆波兰式
- 实现一下类似IDEA插件Rainbow Brackets的效果
由于我们那复杂而脆弱的语法解析器已经写好了,我实在是不想因为这样的需求而对语法解析器大改特改。所以,我们这次用访问者模式对语法解析器进行改造。
程序改造
定义访问者接口
首先,我们要定义一下访问者接口,因为我们一共有Operator
、DataNode
、StaticNode
、FunctionNode
这四种实际的节点,所以在访问者接口里也要体现这四种节点。
/**
* 节点访问器
*/
public interface NodeVisitor {
/**
* 访问操作符
* @param operator
*/
void visitOperator(Operator operator);
/**
* 访问数据节点
* @param dataNode
*/
void visitDataNode(DataNode dataNode);
/**
* 访问常量节点
* @param staticNode
*/
void visitStaticNode(StaticNode staticNode);
/**
* 访问函数节点
* @param functionNode
*/
void visitFunctionNode(FunctionNode functionNode);
}
对节点进行少量的改造
在Node接口中多了这样的一个方法用来接收访问者。
然后,我们在不同的节点中都实现一下这个接口。
- 操作符节点
- 变量节点
- 数据节点
- 函数节点
需要注意的是,每个节点在accept
访问者之后,都只会调用访问者访问对应节点的方法。比如StaticNode
调用的是访问者的visitStaticNode
、FunctionNode
调用的是访问者的visitFunctionNode
。
实现Rainbow Brackets
public class PrettyPrintVisitor implements NodeVisitor{
private static final int[] COLORS = new int[]{31,32,33,34,35};
private int index=0;
private void colorLeftBrackets(){
System.out.print((char)27+"["+ COLORS[index%5]+"m("+(char) 27+"[0m");
index++;
}
private void colorRightBrackets(){
index--;
System.out.print((char)27+"["+ COLORS[index%5]+"m)"+(char) 27+"[0m");
}
@Override
public void visitOperator(Operator operator) {
Node leftNode = operator.getLeftNode();
int currentPriority = operator.priority();
boolean showBrackets = false;
if(leftNode instanceof Operator){
if (((Operator) leftNode).priority()< currentPriority) {
colorLeftBrackets();
showBrackets = true;
}
}
leftNode.accept(this);
if (showBrackets) {
colorRightBrackets();
showBrackets = false;
}
System.out.print(" "+operator.operator()+" ");
Node rightNode = operator.getRightNode();
if(rightNode instanceof Operator){
if (((Operator) rightNode).priority()<= currentPriority) {
colorLeftBrackets();
showBrackets = true;
}
}
rightNode.accept(this);
if (showBrackets) {
colorRightBrackets();
}
}
@Override
public void visitDataNode(DataNode dataNode) {
System.out.print(dataNode.getText());
}
@Override
public void visitStaticNode(StaticNode staticNode) {
System.out.print(staticNode.getText());
}
@Override
public void visitFunctionNode(FunctionNode functionNode) {
System.out.print(functionNode.getFuncName());
colorLeftBrackets();
for (int i = 0; i < functionNode.getParams().size(); i++) {
functionNode.getParams().get(i).accept(this);
if(i<functionNode.getParams().size()-1){
System.out.print(",");
}
}
colorRightBrackets();
}
}
实现逆波兰式打印
public class ReversePolishVisitor implements NodeVisitor {
@Override
public void visitOperator(Operator operator) {
operator.getLeftNode().accept(this);
operator.getRightNode().accept(this);
System.out.print(operator.getText());
}
@Override
public void visitDataNode(DataNode dataNode) {
System.out.print(dataNode.getText());
}
@Override
public void visitStaticNode(StaticNode staticNode) {
System.out.print(staticNode.getText());
}
@Override
public void visitFunctionNode(FunctionNode functionNode) {
System.out.print(functionNode.getText());
}
}
Main
public class VisitorMain {
public static void main(String[] args) {
String exp = "(2-1)*3+2+(3*(9-(5+2)*1))";
Node parse = Calculation.parse(exp);
parse.accept(new PrettyPrintVisitor());
System.out.println();
String exp2 = "Max(8,Max(5,4))+plus100 (max(3,9))";
Node parse2 = Calculation.parse(exp2);
parse2.accept(new PrettyPrintVisitor());
System.out.println();
String exp3 = "2-(3-4)";
Node parse3 = Calculation.parse(exp3);
parse3.accept(new PrettyPrintVisitor());
System.out.println();
String exp4 = "(2-1)*3+2+(3*(9-(5+2)*1))";
Node parse4 = Calculation.parse(exp4);
parse4.accept(new ReversePolishVisitor());
System.out.println();
}
}
运行结果
从运行结果上看,Rainbow Brackets和逆波兰式都成功了
思考
- 访问者模式本身不是一个大众化的设计模式,应用场景相对较窄。
- 如果
Node
的类型多了一个,那NodeVisitor
中的方法也得对应多一个。所以访问者模式适用于结构不怎么会发生变化的场景,比如上面这个语法树的场景。 - 访问者模式遵循开闭原则和单一职责原则,但本身也会变得比较复杂。