第四章:规则语言设计——DSL设计原则、语法定义、操作符支持、函数扩展

好,咱们进入第四章。这一章聊的是规则引擎的“灵魂”——规则语言,也就是DSL。

说实话,我见过太多团队,一上来就搞了个“看起来很强大”的规则配置界面,结果业务方用起来骂娘,开发维护起来想哭。为什么?因为规则语言没设计好。

DSL设计,说白了就是给业务人员一把趁手的“武器”。它既要足够简单,让不懂代码的人也能看懂;又要足够强大,能表达复杂的风控逻辑。这个平衡点,我摸索了好几年才找到点感觉。

4.1 DSL设计原则:少即是多

我个人习惯,设计DSL之前先想清楚三个问题:

  • 谁在用?——风控运营、业务分析师,不是程序员
  • 用来干嘛?——描述“如果…那么…”的规则逻辑
  • 多复杂?——大部分规则是简单条件组合,极少数需要复杂计算

基于这三点,我总结了四条核心原则:

原则 说明 我的经验
可读性优先 规则要像自然语言一样易读 我曾经见过一条规则写了200行,没人敢改
最小表达力 只提供必要的语法元素,不贪多 加一个语法特性,就多一份学习成本
类型安全 编译期就能发现类型错误 运行时才发现“金额”和“字符串”比大小?太晚了
可扩展 支持自定义函数和操作符 业务需求永远比你想象的更奇葩

核心观点:DSL不是编程语言,它是一层“业务翻译器”。设计时多想想业务人员怎么读,少想自己怎么炫技。

4.2 语法定义:从“人话”到“机器话”

嗯,这里我直接给出一套我常用的语法结构。它借鉴了SQL和自然语言的混合风格,经过多个项目验证,业务方上手很快。

4.2.1 规则基本结构

// 一条完整的规则
rule "高风险交易拦截" {
    when
        transaction.amount > 10000
        AND user.riskLevel == "high"
        AND NOT user.isWhitelisted
    then
        result = "reject"
        reason = "大额高风险交易"
        priority = 1
}

你看,这个结构是不是很像在说人话?when后面跟条件,then后面跟结果。业务人员稍微培训一下就能写。

4.2.2 数据类型支持

我建议至少支持这几种基础类型:

  • 数值型:整数、浮点数(金额、次数、时间戳)
  • 字符串型:设备ID、IP地址、用户名
  • 布尔型:true/false(是否黑名单、是否命中)
  • 集合型:列表、集合(设备指纹列表、IP白名单)
  • 时间型:日期、时间戳、时间间隔

避坑指南:我曾经在项目中只支持了字符串和数值,结果业务方要写“最近30天内的交易次数”这种时间窗口逻辑,硬生生用字符串拼接实现,维护成本爆炸。所以时间类型一定要原生支持。

4.3 操作符支持:够用就好

操作符是DSL的“动词”。我见过有些DSL搞了50多种操作符,结果80%没人用。我的原则是:覆盖90%的场景,剩下的用函数扩展

4.3.1 基础操作符

类别 操作符 示例
比较 ==, !=, >, <, >=, <= amount > 5000
逻辑 AND, OR, NOT age > 18 AND city == "北京"
集合 IN, NOT IN, CONTAINS deviceId IN blacklist
字符串 MATCHES, STARTS_WITH, ENDS_WITH email MATCHES ".*@company.com"
空值 IS NULL, IS NOT NULL phone IS NOT NULL

4.3.2 时间窗口操作符

这个我得重点说说。风控里大量场景是“过去N分钟内做了X次”。我设计了一个专门的时间窗口操作符:

// 过去5分钟内,同一IP登录失败超过3次
rule "暴力破解检测" {
    when
        count(loginEvent, 
             where: event.ip == current.ip 
             AND event.status == "failed",
             within: 5 minutes) > 3
    then
        result = "block"
        reason = "疑似暴力破解"
}

你想想看,如果没有这个操作符,业务方得写多复杂的逻辑?自己维护时间窗口、滑动计数…嗯,交给引擎去做就好。

4.4 函数扩展:给DSL装上“外挂”

再强大的DSL也覆盖不了所有场景。所以函数扩展能力是必须的。我把它分为两类:

4.4.1 内置函数

这些是引擎自带的,开箱即用:

  • 数学函数:abs(), round(), max(), min()
  • 字符串函数:length(), substring(), toLower(), toUpper()
  • 集合函数:size(), containsAny(), containsAll()
  • 风控专用:geoDistance(), ipToCity(), deviceFingerprint()
// 内置函数示例
rule "异地登录检测" {
    when
        geoDistance(user.lastLoginCity, current.city) > 1000
        AND timeDiff(current.time, user.lastLoginTime) < 30 minutes
    then
        result = "review"
        reason = "疑似异地登录"
}

4.4.2 自定义函数扩展

这才是真正的杀手锏。我设计了一个函数注册机制,开发可以随时添加新函数:

// Java端注册自定义函数
public class CustomFunctions {
    
    @RuleFunction(name = "riskScore", description = "计算综合风险评分")
    public double calculateRiskScore(Map<String, Object> context) {
        double score = 0.0;
        // 复杂的评分逻辑...
        return score;
    }
    
    @RuleFunction(name = "isHoliday", description = "判断当前日期是否为节假日")
    public boolean isHoliday(Date date) {
        // 调用外部节假日API
        return holidayService.check(date);
    }
}

// DSL中使用自定义函数
rule "节假日特殊处理" {
    when
        isHoliday(today)
        AND transaction.amount > 50000
    then
        result = "review"
        reason = "节假日大额交易"
}

注意:自定义函数虽然强大,但也要控制数量。我见过一个项目注册了200多个自定义函数,最后没人知道哪个函数是干嘛的。建议建立函数目录,定期清理废弃函数。

4.5 知识体系总览

说了这么多,咱们用一张图把DSL设计的核心脉络串起来:

DSL规则语言设计核心架构 设计原则 可读性优先 · 最小表达力 · 类型安全 · 可扩展 语法定义 规则结构 (rule-when-then) · 数据类型 (数值/字符串/布尔/集合/时间) 条件表达式 · 结果动作 操作符支持 比较: ==, !=, >, < 逻辑: AND, OR, NOT 集合: IN, CONTAINS 时间窗口: within, count 函数扩展 内置函数: 数学/字符串/集合 风控专用: geoDistance, ipToCity 自定义函数: @RuleFunction注解 函数注册与生命周期管理 规则执行引擎 语法解析 → 类型检查 → 优化 → 执行

4.6 实战中的几个坑

最后,分享几个我踩过的坑,希望能帮你省点时间:

  1. 别让DSL太“灵活”——我见过有人把DSL设计成了图灵完备的语言,结果业务方写出来的规则比代码还难懂。记住,DSL是“领域特定”的,不是通用编程语言。
  2. 错误信息要友好——规则写错了,给个“语法错误”就完事了?我后来改成“第3行,金额字段不能与字符串比较,建议使用数值类型”,业务方自己就能改。
  3. 版本管理不能忘——规则也是资产,需要版本控制。我曾经因为没做版本管理,一次误操作导致线上规则全乱了,回滚花了两个小时。
  4. 性能测试要早做——DSL解析是有开销的。我记得有个项目上线前没压测,结果规则一多,解析时间占了总执行时间的60%。后来加了缓存和预编译才解决。

我的建议:刚开始别追求大而全。先设计一套能覆盖80%场景的DSL,跑起来再说。随着业务发展,再逐步扩展。迭代比完美更重要。

好了,DSL设计这块就聊到这儿。记住,好的DSL是让业务人员觉得“这玩意儿真简单”,而不是让开发觉得“这玩意儿真强大”。方向对了,后面的事情就好办了。

专注资料整理