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 设计框架。每次设计新规则库,我都会先画一遍这个图,确保没有遗漏。

规则定义语言核心逻辑 DSL 设计原则 最小表达原则 显式优于隐式 可组合性 定义方式 YAML(存储/编辑) JSON(传输/运行时) 规则模板化 变量占位符 模板继承 三条原则 + 两种格式 + 模板化 = 可维护的规则定义体系 原则指导设计,YAML/JSON 承载定义,模板化消除重复 一条完整规则 = 元信息 + 数据源 + 校验条件 + 失败处理 rule_id / name / severity | source / target | condition | on_failure

4.6 避坑指南:我踩过的几个坑

最后,分享几个真实教训。希望能帮你少走弯路。

  • 别让 DSL 太灵活:我曾经设计过一个 DSL,允许在 condition 里写正则表达式。结果有人写了个灾难性的回溯正则,直接把校验引擎拖垮了。后来我加了限制:正则必须预编译,且超时 100ms 自动终止。
  • 版本管理要跟上:规则文件也是代码,必须进 Git。我见过有人直接在服务器上改 YAML,改完忘了备份,回滚的时候一脸懵。记住:一切规则变更都要走 PR。
  • 模板变量要有默认值:如果某个变量没填,系统应该能优雅降级,而不是直接抛异常。我习惯给每个变量设一个安全的默认值,比如 severity 默认 warning,这样即使漏填,也不会阻断流程。
  • 测试你的模板:模板本身也要有单元测试。写一个测试用例,传入不同的变量组合,验证展开后的规则是否符合预期。这个我后面讲 CI/CD 的时候会详细展开。

一句话总结:好的 DSL,让写规则像填表一样简单;好的模板,让维护规则像搭积木一样轻松。