设计模式与编译原理:解释器模式解析语法、访问者模式遍历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. 三者如何配合?一张图看懂

这三个模式在编译流程中是这样配合的:

设计模式在编译流程中的配合 解释器模式 解析语法 输入:源代码字符串 输出:AST 访问者模式 遍历AST 输入:AST 输出:类型检查结果 策略模式 选择优化 输入:AST 输出:优化后AST 详细流程说明: 1. 解释器模式:将源代码解析为抽象语法树(AST),定义文法规则 2. 访问者模式:遍历AST,执行类型检查、语义分析等操作 3. 策略模式:根据编译选项选择不同的优化策略(如-O0、-O1、-O2) 4. 三者配合:解释器构建结构,访问者分析结构,策略优化结构 注:实际编译器中还有更多阶段,这里只展示与设计模式相关的核心流程

5. 实际项目中的取舍

这三个模式虽然好用,但也不是银弹。我总结了几点经验:

模式 适用场景 不适用场景
解释器模式 语法规则固定、执行频率不高的DSL 语法复杂、性能要求高的场景
访问者模式 数据结构稳定、操作频繁变化的场景 数据结构经常变化、操作固定的场景
策略模式 算法需要动态切换、避免大量if-else 算法数量很少且不会变化

我的建议:如果你在做一个规则引擎或者简单的脚本语言,这三个模式是绝配。但如果是做生产级的编译器(比如GCC、LLVM),它们内部用的是更复杂的架构,设计模式只是其中的一部分思想。

好了,今天就聊到这儿。这三个模式配合起来,能帮你写出结构清晰、易于扩展的代码。下次遇到需要解析、分析、优化的场景,不妨试试这个组合拳。


公众号:蓝海资料掘金营,微信deep3321