设计模式与编译原理:解释器模式解析语法、访问者模式遍历AST、策略模式选择优化
说实话,很多程序员一听到「编译原理」四个字就头大。我当年在学校也是,觉得这东西离实际开发太远了。直到后来做了一套规则引擎,才恍然大悟——原来设计模式和编译原理可以配合得这么默契。
今天咱们就聊聊三个模式:解释器模式、访问者模式和策略模式。它们在编译器的不同阶段各司其职,配合起来堪称完美。
1. 解释器模式:把字符串变成可执行逻辑
解释器模式的核心思想很简单:定义一个语言的文法,然后解释执行它。说白了,就是给你一段字符串,你能把它解析成有意义的操作。
我在项目中遇到过这样一个场景:业务方需要自定义一些过滤规则,比如「年龄大于18且城市等于北京」。如果每次都改代码,那运维就疯了。于是我用解释器模式搞了个简单的规则引擎。
先看一个简化版的表达式接口:
// 抽象表达式
interface Expression {
boolean interpret(Context ctx);
}
// 终端表达式:变量
class VariableExpression implements Expression {
private String name;
public VariableExpression(String name) {
this.name = name;
}
@Override
public boolean interpret(Context ctx) {
return ctx.getVariable(name);
}
}
// 非终端表达式:AND
class AndExpression implements Expression {
private Expression left;
private Expression right;
public AndExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public boolean interpret(Context ctx) {
return left.interpret(ctx) && right.interpret(ctx);
}
}
你想想看,这种结构其实就是一个树形结构。每个节点要么是叶子(变量),要么是内部节点(操作符)。解释器模式就是沿着这棵树递归往下走,最终算出结果。
核心要点:解释器模式适合那些语法规则相对固定的场景。如果语法经常变,那维护成本会很高。我建议用ANTLR之类的工具生成解析器,而不是手写。
2. 访问者模式:在不修改AST节点的情况下增加操作
解释器模式解析完语法后,我们得到了一个抽象语法树(AST)。接下来要对这棵树做各种操作——比如类型检查、代码生成、优化等等。
问题来了:AST的节点类型是固定的(比如有IfNode、WhileNode、AssignNode),但操作却在不断增加。如果每次加操作都去改节点类,那就违反了开闭原则。
访问者模式就是来解决这个问题的。它把操作从节点中抽离出来,放到一个「访问者」里。
// 抽象访问者
interface ASTVisitor {
void visit(IfNode node);
void visit(WhileNode node);
void visit(AssignNode node);
}
// 抽象节点
abstract class ASTNode {
abstract void accept(ASTVisitor visitor);
}
// 具体节点
class IfNode extends ASTNode {
@Override
void accept(ASTVisitor visitor) {
visitor.visit(this);
}
}
// 具体访问者:类型检查
class TypeCheckVisitor implements ASTVisitor {
@Override
public void visit(IfNode node) {
System.out.println("检查If节点的类型...");
// 检查条件表达式是否为boolean
}
@Override
public void visit(WhileNode node) {
System.out.println("检查While节点的类型...");
}
@Override
public void visit(AssignNode node) {
System.out.println("检查赋值节点的类型...");
}
}
嗯,这里要注意:访问者模式的核心是双重分派。第一次分派是调用节点的accept方法,第二次分派是调用访问者的visit方法。这样就把操作和数据结构解耦了。
我的经验:如果你发现AST节点类里有一堆if-else判断类型,那就该考虑访问者模式了。我曾经接手过一个老项目,一个节点类里有2000多行代码,全是各种操作。用访问者模式重构后,每个访问者只有几百行,清晰多了。
3. 策略模式:让优化算法可插拔
AST遍历完了,接下来要优化。比如常量折叠、死代码消除、循环展开等等。不同的优化策略适用于不同的场景。
策略模式在这里就派上用场了。它把每个优化算法封装成一个策略,可以在运行时自由切换。
// 优化策略接口
interface OptimizationStrategy {
ASTNode optimize(ASTNode node);
}
// 常量折叠策略
class ConstantFoldingStrategy implements OptimizationStrategy {
@Override
public ASTNode optimize(ASTNode node) {
// 如果节点是 AddNode(Const(1), Const(2)),直接替换为 Const(3)
System.out.println("执行常量折叠...");
return node;
}
}
// 死代码消除策略
class DeadCodeEliminationStrategy implements OptimizationStrategy {
@Override
public ASTNode optimize(ASTNode node) {
// 删除永远不会执行的代码
System.out.println("执行死代码消除...");
return node;
}
}
// 优化器
class Optimizer {
private List<OptimizationStrategy> strategies;
public Optimizer(List<OptimizationStrategy> strategies) {
this.strategies = strategies;
}
public ASTNode optimize(ASTNode root) {
ASTNode current = root;
for (OptimizationStrategy strategy : strategies) {
current = strategy.optimize(current);
}
return current;
}
}
说白了,策略模式就是把算法定义成一系列可互换的类。在编译器优化阶段,我们可以根据编译选项(比如-O0、-O1、-O2)来选择不同的策略组合。
避坑指南:我曾经犯过一个错误——把所有优化策略都放在一个类里,用if-else控制。结果代码越来越臃肿,测试也不好写。后来改成策略模式,每个策略独立测试,爽多了。记住:策略之间最好是无状态的,否则容易出并发问题。
4. 三者如何配合?一张图看懂
这三个模式在编译流程中是这样配合的:
5. 实际项目中的取舍
这三个模式虽然好用,但也不是银弹。我总结了几点经验:
| 模式 | 适用场景 | 不适用场景 |
|---|---|---|
| 解释器模式 | 语法规则固定、执行频率不高的DSL | 语法复杂、性能要求高的场景 |
| 访问者模式 | 数据结构稳定、操作频繁变化的场景 | 数据结构经常变化、操作固定的场景 |
| 策略模式 | 算法需要动态切换、避免大量if-else | 算法数量很少且不会变化 |
我的建议:如果你在做一个规则引擎或者简单的脚本语言,这三个模式是绝配。但如果是做生产级的编译器(比如GCC、LLVM),它们内部用的是更复杂的架构,设计模式只是其中的一部分思想。
好了,今天就聊到这儿。这三个模式配合起来,能帮你写出结构清晰、易于扩展的代码。下次遇到需要解析、分析、优化的场景,不妨试试这个组合拳。
公众号:蓝海资料掘金营,微信deep3321