手写一个表达式计算器,你就理解解释器模式了

手写一个表达式计算器,你就理解解释器模式了

写过计算器的都知道,解析一个数学表达式有多麻烦。加减乘除、括号优先级、变量替换......如果你用一堆 if-else 来硬刚,代码会膨胀到让你自己都看不懂。

解释器模式(Interpreter Pattern)就是干这个活的:给一种"语言"定义语法规则,然后用代码来解释执行它。

解释器模式到底解决什么问题

先说一个真实场景:你做了一个电商促销系统,运营想自己配促销规则,比如"满200减30,SKU 为 A001 的商品打8折,VIP 用户再减10"。你不可能每次运营改规则都去发版上线对吧?

你需要一种方式,让运营配置的规则能被系统直接"读懂"并执行。这就是解释器模式的核心------把业务规则变成一种小语言,然后写一个解释器去执行它

再举几个你一定见过的例子:

  • SQL 查询语句,数据库引擎就是 SQL 的解释器
  • 正则表达式,regex 引擎在解释你写的模式串
  • 模板引擎(FreeMarker、Thymeleaf),解释模板里的 ${} 占位符
  • Spring 的 SpEL(#{#user.age > 18}),在运行时解析表达式

这些本质上都是解释器模式的应用。

解释器模式的核心结构

解释器模式只有两个关键角色:

  • AbstractExpression(抽象表达式) :定义统一的 interpret() 方法
  • TerminalExpression(终结符表达式):处理语法中最小的单位,比如数字、变量名
  • NonTerminalExpression(非终结符表达式):处理语法中的组合规则,比如加法、减法

再加一个 Context(上下文),用来存运行时的变量和状态。

画个图就是:

复制代码
Context(存变量)
   |
AbstractExpression(interpret 方法)
   ├── TerminalExpression(数字/变量,直接返回值)
   └── NonTerminalExpression(运算符,递归调用子表达式的 interpret)

动手写一个:四则运算解释器

光看概念太虚了,直接上代码。我们实现一个支持加减乘除和括号的表达式解释器。

先定义抽象表达式:

java 复制代码
public interface Expression {
    int interpret(Map<String, Integer> context);
}

然后是终结符表达式------处理数字和变量:

java 复制代码
// 数字
public class NumberExpression implements Expression {
    private final int number;

    public NumberExpression(int number) {
        this.number = number;
    }

    @Override
    public int interpret(Map<String, Integer> context) {
        return number;
    }
}

// 变量
public class VariableExpression implements Expression {
    private final String name;

    public VariableExpression(String name) {
        this.name = name;
    }

    @Override
    public int interpret(Map<String, Integer> context) {
        return context.getOrDefault(name, 0);
    }
}

非终结符表达式------加减乘除:

java 复制代码
public class AddExpression implements Expression {
    private final Expression left;
    private final Expression right;

    public AddExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Map<String, Integer> context) {
        return left.interpret(context) + right.interpret(context);
    }
}

减法、乘法、除法结构完全一样,只是把 + 换成对应的运算符,代码就不贴了。

把这几个组装起来,解析 "a + b * 3 - (c - 2)" 这样的表达式:

java 复制代码
Map<String, Integer> vars = new HashMap<>();
vars.put("a", 10);
vars.put("b", 4);
vars.put("c", 5);

// 对应表达式:a + b * 3 - (c - 2)
Expression expr = new SubtractExpression(
    new AddExpression(
        new VariableExpression("a"),
        new MultiplyExpression(
            new VariableExpression("b"),
            new NumberExpression(3)
        )
    ),
    new SubtractExpression(
        new VariableExpression("c"),
        new NumberExpression(2)
    )
);

int result = expr.interpret(vars);
// 10 + 4*3 - (5-2) = 10 + 12 - 3 = 19

你看,解释器把表达式变成了一个语法树 。每个节点都是一个 Expressioninterpret() 递归向下计算,最后得出结果。

实际踩过的坑

第一版代码我写的很兴奋,觉得解释器模式太优雅了。然后运营配了一条规则:价格 > 100 AND (分类 = 电子产品 OR 品牌 = 苹果)

我一看傻了------我那套只支持四则运算的解释器完全不够用。要支持 AND、OR、比较运算符,还得处理字符串类型的变量......

这里就是解释器模式最大的痛点:语法规则越复杂,解释器的类就越膨胀。每加一种语法就要加一个类,维护成本直线上升。

后来我换了个思路:不再自己写解释器,直接用了 ANTLR 。它是一个强大的语法分析工具,你只需要写语法规则文件(.g4),它自动帮你生成解析器代码。而且它生成的解析器性能比自己手写的好得多。

但这不代表解释器模式就没用了。理解了解释器模式的原理,你才知道 ANTLR 这类工具在底层帮你做了什么。而且对于简单的场景(比如只支持 AND/OR 的条件表达式),手写解释器反而是更轻量的选择。

你什么时候该用解释器模式

用一个简单的判断标准:

  • 你需要解析一种"小语言"(不是完整的编程语言)
  • 语法规则相对简单,用编译原理那套 yacc/lex 太重了
  • 业务规则频繁变化,你不想每次都改代码重新部署

典型的应用场景:

  1. 规则引擎:促销规则、风控规则、审批流程条件
  2. SQL/NoSQL 查询构建器:把对象查询条件转成 SQL
  3. 模板表达式 :解析 #{user.name} 之类的占位符
  4. 配置表达式 :比如 Nginx 配置中 $variable 的解析

什么时候别用

这个我要说句实话:解释器模式是 23 种设计模式里使用频率最低的之一

原因很简单:大多数需要"解释"的场景,都已经有了成熟的工具。

  • 要解析 SQL?用 MyBatis/Hibernate
  • 要解析表达式?用 Spring EL / OGNL / SpEL
  • 要做规则引擎?用 Drools / LiteFlow
  • 要做模板渲染?用 FreeMarker / Thymeleaf

除非你做的产品本身就是一个 DSL(领域特定语言)编辑器,或者你的规则系统有特殊需求现有工具覆盖不了,否则手写解释器大概率是过度设计。

我在实际项目中只手写过一次解释器------是一个极其简单的条件过滤器,只支持 field op value 的格式(age > 18status == active),连括号都没有。就这么简单的东西,用 ANTLR 都嫌重,手写反而是最优解。

一句话总结

解释器模式教会你的不是"怎么手写一个完整的语言解释器",而是让你理解语法树、递归解析、上下文传递这些概念。这些概念在你看 ANTLR、看编译原理、看 SpEL 源码的时候,会让你觉得"哦原来底层是这样"。

理解原理比背实现重要得多。


我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,目前正在开发中。如果你觉得这类内容有意思,搜一下「爪爪代码冒险记」,或者等我后面的文章------每篇文章我都会同步对应的小程序内容。

相关推荐
绛洞花主敏明1 小时前
Go操作xorm中间表多对多关联实战
开发语言·后端·golang
长栎1 小时前
foreach 语法糖背后,迭代器模式做了多少脏活
后端
HLAIA光子1 小时前
LLM缓存机制:你的API账单可以砍掉75%
后端·llm·ai编程
卷无止境1 小时前
统计质量控制(SQC / SPC):用数据说话的质量哲学
后端
XovH1 小时前
第 44篇 k8s之实战:将 Web 应用迁移到 Kubernetes(上)
后端
晓杰'1 小时前
从0到1实现Balatro游戏后端(7):Boss Blind与特殊规则实现
后端·websocket·typescript·node.js·游戏开发·项目实战·nestjs
MariaH1 小时前
Node.js 架构理解
后端
我登哥MVP1 小时前
Spring Boot 从“会用”到“精通”:请求映射原理
java·spring boot·后端·spring·servlet·maven·intellij-idea
MariaH1 小时前
Node-fs模块
后端