原来的报价规则写死为“price - 10 + 5”,100 分的结果是 95。配置方现在想写“price - 2 * 3”表达另一种固定折扣。若直接按文本从左到右执行,会把先减后乘算成 294,而要求乘法优先的结果是 94。为这点变化建立小语言值不值得?首先要把能写什么和不能写什么划清。

限定文法,不运行任意源码

本章文法仅支持 price、非负十进制整数、+、-、* 和空格:expression := term ((+ | -) term)*,term := atom ('*' atom)*,atom := price | integer。没有括号、除法、变量名、函数调用或任何反射/脚本执行。labs/38 的 After 用递归下降先解析乘法项,再组合加减表达式树;Plus/Minus/Times 在求值时使用 Math.*Exact,负输入或负最终结果拒绝,超过 64 个字符也拒绝。节点仅有教学用途,并不对外提供脚本权限。

1
2
int oldPrice = new Scenario.After("price - 10 + 5").evaluate(100);
int newPrice = new Scenario.After("price - 2 * 3").evaluate(100);

Before.evaluate(100) 是直接写死旧公式的最小实现,适合规则固定时;Alternative.quote(100) 也是直接函数,要求从代码里选规则而不允许输入表达式。只有规则确需作为受限的配置文本保存、读取和演进,解析器的语法树成本才值得承担。Interpreter 在这里指小语言的表达式组合及求值,和 Visitor 一样可处理树,但关注的不是报表新增操作,而是固定语法规则怎样解释输入。

输入 旧版/替代函数 新解释器
100 分,旧公式 95 95
price - 2 * 3 代码须另写规则 94,先乘后减
price + evil、price / 0 非受支持输入 拒绝而非执行外部内容

单次调用可在同一解释器实例上用不同基价重新求值;但不做并发共享对象测试。表达式长度限制并不能替代所有安全审计,解析器没有括号、Unicode 数字和真实货币舍入规范。

flowchart LR
    Caller[表达式调用方] --> Parser[After 解析受限文法]
    Parser --> Tree[Expression 语法树]
    Tree --> Binary[Plus / Minus / Times]
    Binary --> Child[左右子 Expression]
    Tree --> Terminal[Price / NumberValue]
    Tree --> Evaluate[对 price 求值]

验证与练习

在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/38 -am test 以及累计 ./mvnw -B -ntp verify。原始输出、版本、非法输入、溢出和边界见 examples/design-patterns/evidence/38/RUN.md;不以“能解析”冒充安全沙箱。

  1. 新增受限括号语法,先给旧优先级、未知输入与 64 字符上限加回归,再为 (price - 2) * 3 写新断言;禁止引入任意代码执行。
  2. 若配置方最终只允许两种预置折扣,删掉解析器改为两个普通函数,保留旧金额和错误边界断言,说明哪个维护成本实际降低。

参考资料

上一节:37 新操作与新节点哪个更贵;下一节:39 从变化需求保留必要模式。