设计模式 38:折扣表达式需要语法树吗
原来的报价规则写死为“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 | |
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;不以“能解析”冒充安全沙箱。
- 新增受限括号语法,先给旧优先级、未知输入与 64 字符上限加回归,再为
(price - 2) * 3写新断言;禁止引入任意代码执行。 - 若配置方最终只允许两种预置折扣,删掉解析器改为两个普通函数,保留旧金额和错误边界断言,说明哪个维护成本实际降低。
参考资料
- GoF 原书公开图书馆 PDF,5.3 Interpreter,目录页标注起始页 274:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- Interpreter 模式简定义,InfoWorld/JavaWorld:https://www.infoworld.com/article/2170730/design-patterns-the-big-picture-part-1-design-pattern-history-and-classification.html 。
- 本篇来源边界:
writing-plans/design-patterns/SOURCES.md。 - 实验代码:
examples/design-patterns/labs/38/。
上一节:37 新操作与新节点哪个更贵;下一节:39 从变化需求保留必要模式。






