4. 规则定义语言:DSL设计原则、YAML/JSON定义规则、规则模板化
好,我们进入第四章。这一章聊的是规则怎么「写」的问题。
你想想看,一个数据校验平台,如果每次加规则都要改代码、重新部署,那还叫什么持续集成?所以,我们需要一门规则定义语言——也就是 DSL(Domain Specific Language)。
说白了,就是让业务人员也能看懂、能写的东西。我见过太多团队,规则写在 Java 代码里,每次改个阈值都要找开发,排期两三天。嗯,这其实是个流程问题,但根源在技术选型上。
4.1 DSL 设计原则:别把它搞成另一门编程语言
设计 DSL 的时候,我给自己定了三条铁律。这些年踩过的坑,基本都能归到这三条里。
- 最小表达原则:只描述「校验什么」和「怎么校验」,不要引入变量、循环、函数调用。你一旦允许在规则里写 for 循环,后面就有人敢在里面写递归。
- 显式优于隐式:每个字段的含义必须一眼能看懂。我曾经见过一个 DSL,用
op: 3表示「大于」,结果三个月后没人记得 3 是什么。从那以后,我坚持用字符串枚举,比如operator: "greater_than"。 - 可组合性:简单规则能拼成复杂规则,但不能互相干扰。这个后面讲模板化的时候会细说。
核心观点:DSL 是给人类读的,不是给机器读的。机器只是顺便执行一下。
4.2 YAML vs JSON:我为什么偏向 YAML
这个问题几乎每次培训都有人问。我的答案很直接:YAML 更适合写规则,JSON 更适合传规则。
你看,规则是要被人反复阅读、修改的。YAML 的缩进结构天然适合表达层级关系,而且支持注释——这个太重要了。你可以在规则旁边写一行 # 2024-08-01 根据业务方要求调整阈值,三个月后回头看,就知道当时为什么这么改。
JSON 呢?也不是不能用。如果你的规则是从某个配置中心直接下发,或者要经过 API 网关,JSON 的解析性能更好,生态也更成熟。但写注释?抱歉,JSON 不支持。
我个人习惯:规则库存储用 YAML,运行时传输用 JSON。CI/CD 流水线里加一步 YAML-to-JSON 的转换,两边的好处都占了。
| 特性 | YAML | JSON |
|---|---|---|
| 可读性 | 高(缩进结构,无括号噪音) | 中(大量花括号和引号) |
| 注释支持 | 支持(# 号注释) | 不支持 |
| 解析性能 | 中(需要处理缩进和类型推断) | 高(原生支持,解析器成熟) |
| 适用场景 | 规则定义、配置文件 | API 传输、配置中心存储 |
4.3 用 YAML 定义一条规则:从零开始
光说不练假把式。我们直接看一条完整的规则长什么样。
# 规则:订单金额不能为负数
rule_id: "R001"
name: "订单金额非负校验"
description: "检查 order.amount 字段,确保其值大于等于 0"
enabled: true
severity: "error" # error / warning / info
source:
type: "kafka"
topic: "order_created"
format: "avro"
target:
field: "order.amount"
type: "double"
condition:
operator: "greater_than_or_equal"
value: 0.0
on_failure:
action: "reject" # reject / alert / tag
notify: ["duty@team.com"]
tags: ["金额异常", "实时校验"]
你看,每一段都很清楚。source 告诉系统数据从哪来,target 告诉它校验哪个字段,condition 是校验逻辑,on_failure 是出问题怎么办。
这里有个细节:severity 字段。我建议至少分三级——error 阻断流程,warning 只告警不阻断,info 记录日志。为什么?因为不是所有规则都值得让流水线停下来。我曾经见过一个团队,把所有规则都设成 error,结果某个字段偶尔为空,整条链路天天断。后来改成 warning,问题照查,业务照跑,两边都满意。
小技巧:condition 里的 operator 建议用枚举值,不要用符号。比如用 "greater_than" 而不是 ">"。原因很简单:符号容易跟 YAML 的语法冲突,而且不同语言对符号的转义规则不一样。
4.4 规则模板化:别重复造轮子
你很快会发现,很多规则长得差不多。比如「字段 A 不能为空」、「字段 B 必须在某个范围内」、「字段 C 的格式必须是邮箱」。如果每条都从头写,维护成本会直线上升。
所以,我们需要模板。
模板是什么?就是一个半成品的规则,把可变的部分挖成变量。比如:
# 模板:非空校验
template_id: "T001"
name: "非空校验模板"
description: "检查指定字段是否为 null 或空字符串"
variables:
- name: "field_name"
type: "string"
description: "要校验的字段名"
- name: "severity"
type: "string"
default: "warning"
condition:
operator: "is_not_null"
field: "{{ field_name }}"
on_failure:
action: "reject"
tags: ["非空校验", "{{ field_name }}"]
使用时,只需要填变量:
rule_id: "R002"
name: "用户邮箱非空校验"
template: "T001"
variables:
field_name: "user.email"
severity: "error"
这样做的好处很明显:
- 一致性:所有非空校验的逻辑完全一致,不会出现「这个用 is_not_null,那个用 length > 0」的混乱。
- 可维护性:如果有一天你要改非空校验的逻辑(比如把空字符串也视为 null),只需要改模板,所有引用它的规则自动生效。
- 降低门槛:业务人员只需要填变量,不需要理解 DSL 的完整语法。
注意:模板不要嵌套太深。我见过有人搞出「模板的模板的模板」,最后一条规则展开后有两百行。嗯,那还不如直接写规则。一般来说,一层模板、一层变量就足够了。
4.5 知识体系:规则定义语言的核心逻辑
下面这张图,是我自己梳理的 DSL 设计框架。每次设计新规则库,我都会先画一遍这个图,确保没有遗漏。
4.6 避坑指南:我踩过的几个坑
最后,分享几个真实教训。希望能帮你少走弯路。
- 别让 DSL 太灵活:我曾经设计过一个 DSL,允许在 condition 里写正则表达式。结果有人写了个灾难性的回溯正则,直接把校验引擎拖垮了。后来我加了限制:正则必须预编译,且超时 100ms 自动终止。
- 版本管理要跟上:规则文件也是代码,必须进 Git。我见过有人直接在服务器上改 YAML,改完忘了备份,回滚的时候一脸懵。记住:一切规则变更都要走 PR。
- 模板变量要有默认值:如果某个变量没填,系统应该能优雅降级,而不是直接抛异常。我习惯给每个变量设一个安全的默认值,比如 severity 默认 warning,这样即使漏填,也不会阻断流程。
- 测试你的模板:模板本身也要有单元测试。写一个测试用例,传入不同的变量组合,验证展开后的规则是否符合预期。这个我后面讲 CI/CD 的时候会详细展开。
一句话总结:好的 DSL,让写规则像填表一样简单;好的模板,让维护规则像搭积木一样轻松。