这几年做业务系统,我越来越警惕一种看起来很省事的路径:为了绕开发版成本,把越来越多逻辑搬进配置里。

起点通常都很合理。版本要兼容,客户端要区分,业务线要有开关,某些场景要灰度,出了问题还想留个兜底。真按现场压力看,配置化几乎总是最自然的第一反应。

问题不在这里。问题在于,这条路很少会停在“只是配个参数”。

一开始是开关,后面要加条件;有了条件,就要支持组合;组合多了,要有优先级;优先级出来了,又要支持嵌套和默认分支。走到这一步,配置已经不只是在补参数了,它开始决定系统怎么跑、哪条业务能走通、哪个请求该被拦住。

这时候你其实已经不是在扩展配置,而是在设计一门语言。

我想写清楚的,就是这件事里最容易被轻描淡写带过去的几个工程判断:DSL 最难的地方在哪,为什么“更灵活”常常把系统做重,以及什么样的 DSL 才值得进生产。

真正变难的,不是配置变多,而是配置开始决定业务行为

很多团队对 DSL 的理解,是“把一些判断写到配置里,省得每次都改代码”。这句话不算错,但它很容易把问题说轻。

因为一旦配置开始承载业务行为,系统里就会同时长出第二套运行逻辑:

  • 它也有条件判断
  • 它也有分支和优先级
  • 它也会影响运行时行为
  • 它也会直接决定业务后果

区别只在于,这套逻辑通常没有成熟代码那样的保护。

它未必有完善的静态校验,未必有足够好的测试方式,未必有 IDE 提示,也未必有顺手的调试能力。很多时候,它甚至连“这次为什么这样执行”都讲不清。

所以我现在看 DSL,最先问的变成了:它是不是已经在系统里平行造出了第二套代码体系。如果答案是,那后面就不能再按“多加几个配置项”来对待了。

DSL 真正难受的地方,不是难写,而是出了问题很难解释

我对这件事印象最深的一次,是之前做一段资产指纹相关逻辑的时候。

那次系统没有挂,服务也没有崩。消息进来了,进程在跑,链路上看不出明显异常,日志里也没有直接报错。可业务结果就是不对,它不消费了。

最后查出来,不是什么底层 bug,也不是什么中间件故障,就是一条配置约束写偏了。

麻烦的地方在于,它不是语法错,也不是系统能识别出来的非法配置。站在执行器的视角,它是合法的;站在业务语义的视角,它又确实写错了。

这类问题最折磨人的地方,就是系统不会提醒你“这里错了”。它只会安静地按错误理解继续执行。你只能倒着猜:

  • 是哪条规则没命中
  • 是不是被更高优先级的规则挡掉了
  • 是不是字段值和自己以为的不一样
  • 是不是默认分支把流量吃掉了
  • 是不是条件写得过严,导致根本进不去

这时候你会意识到,DSL 最大的门槛就在这儿:它有没有把自己的执行过程说清楚。

如果一个 DSL 只能告诉你“命中了规则 A”,却说不出为什么命中、哪些条件没过、有没有被别的规则覆盖、改一个关键字段结果会不会变化,那它其实还是个黑盒。黑盒顺的时候很好用,一旦不顺,排查成本会直接抬起来。

那次排查花了我大半天。最后发现答案就躺在那条规则里,只是系统没有任何办法告诉我。

很多 DSL 不是因为不够强才失败,而是因为太想让人什么都能写

我以前也会天然觉得,DSL 越通用越高级。踩坑多了以后,我开始怀疑这种直觉。

因为所谓“更灵活”,在工程上通常意味着一件事:你把约束放开了。

约束一放开,不同的人就会按不同理解去写规则。同一个业务意图,有人喜欢把条件堆成一条长表达式,有人喜欢拆成多条规则再配优先级,有人写正逻辑,有人写反逻辑,有人依赖默认分支,有人一定要把兜底显式写出来。

系统语法也许是统一的,团队理解却不是统一的。

这类问题一开始不会表现成“系统不能用”,它更常见的症状是另一种慢性失控:

  • review 越来越难
  • 接手越来越难
  • 排查越来越难
  • 修改越来越难
  • 大家越来越怕动它

这就是很多 DSL 最后的状态。它本来是为了更灵活,结果却把复杂度从代码里转移到了配置里,再把这份复杂度分散到团队认知里。

踩过这些坑以后,我对 DSL 的第一要求就是“收敛”。它应该把大家往同一种表达习惯上收,别鼓励每个人都写出自己的方言。

如果 DSL 暴露的只是底层字段,它迟早会把业务方逼成规则工程师

这个问题我现在看得很重。

很多 DSL 表面上是在给业务配规则,实际上暴露出来的却是底层字段和操作符。比如 clientversionbiz_coderisk_levelext_info,再配上 andorincontains 之类的表达能力。

从实现角度看,这没什么问题。可从使用角度看,它经常是在逼人直接拿底层事实去拼业务语义。

业务实际想表达的,通常不是“某个字段等于几”。它想表达的是:

  • 这是老用户
  • 这是首贷场景
  • 这是高风险路径
  • 这类消息当前不该消费
  • 这个能力对当前客户端不开放

这才是业务概念。

如果 DSL 不去承接这些概念,而是要求大家自己在底层字段上拼条件,那么配置迟早会变脆。因为它过度依赖写规则的人是否真的知道每个字段的含义、边界和取值,一旦理解偏一点,配置就会“合法地写错”。

这也是为什么我很在意一条设计原则:好的 DSL,不该鼓励使用者直接操作底层事实,而应该尽量让他们直接表达业务意图。

你让人写“首贷用户”,和让人写“loan_type in [1,2] 且 history_cnt == 0”,工程含义完全不是一回事。前者在表达业务语义,后者在堆实现细节。

我现在更看重的,不是表达力,而是这几条工程约束

如果一定让我说这几年对 DSL 最稳定的判断,我会更看重下面几件事。

先守边界,先回答什么不该进 DSL

不是所有变化都值得配置化,也不是所有规则都值得抽象成语言。

有些东西说白了只是稳定映射,用结构化配置就够了。有些东西就是复杂流程控制,应该老老实实留在代码里。你如果硬把它们塞进 DSL,只是换了一个更难调试的地方继续写逻辑。

成熟团队最稀缺的能力,往往是把 DSL 的边界守住。

强约束,不要开放作文

线上系统最怕的不是语法不够花,而是大家按不同习惯表达同一件事。

所以好的 DSL 应该主动限制:

  • 能用哪些字段
  • 能用哪些操作符
  • 表达式可以复杂到什么程度
  • 能不能嵌套
  • 默认分支怎么写
  • 哪些高歧义组合根本不允许出现

它不该鼓励“只要语法对就行”,而应该鼓励“大家都用同一种可理解的方式表达”。

把失败语义讲清楚,不要让系统停在“没错但也没工作”

这是线上最容易把人拖进泥里的地方。

字段缺失怎么办,依赖值为空怎么办,没命中任何规则怎么办,多条规则同时命中怎么办,默认行为到底是什么,这些都不是边角料。

如果这些语义模糊,系统就很容易滑到一种特别糟糕的状态:不报错,但结果不对。

从工程角度看,这种状态最难排查;从业务角度看,这种状态最难感知;从治理角度看,这种状态也最难界定责任。

可解释、可回放、可 diff,比更强的语法重要

如果团队资源有限,我现在会优先把投入放在这些能力上,而不是继续扩表达式:

  • explain 能力
  • 命中链路还原
  • 样本回放
  • 规则 diff
  • 命中统计
  • 条件失败分布
  • 变更影响面分析

因为这些能力决定了一件事:DSL 出了问题以后,能不能被当成一个工程对象来管理。

很多错误应该拦在上线前,不该等线上事故来教育人

字段不存在、枚举值写错、规则永远命不中、新规则被旧规则完全遮蔽、两条规则明显冲突、某次变更会把一类流量整体带偏,这些问题本来就不该主要依赖线上观察来发现。

一个成熟的 DSL,不该只提供配置入口。它还要尽量提供静态校验、冲突检测、死规则检测、样本模拟、上线前 diff 和影响面评估。

不然它只是把事故发生的位置,从代码发布阶段搬到了配置发布阶段。

把上面几件事串在一起看,配置从长出来到出问题的整条链路大概是这样的:

stateDiagram-v2
    state "开关→条件→组合→优先级→DSL" as evo
    state "配置加载" as load
    state "静态校验" as validate
    state "上线前拦截" as block
    state "规则求值" as eval
    state "命中执行" as hit
    state "走默认分支" as fallback
    state "业务结果正确" as ok
    state "静默失败" as silent
    state "倒查归因" as debug
    state "回滚" as rollback

    [*] --> evo : 配置逐步承载业务行为
    evo --> load : 进入执行态

    load --> validate
    validate --> block : 语法非法或字段不存在
    validate --> eval : 语法合法
    block --> load : 修正重试

    eval --> hit : 条件匹配
    eval --> fallback : 无规则命中或条件过严

    hit --> ok : 语义正确
    hit --> silent : 合法地写错
    fallback --> silent : 默认分支吃掉流量

    silent --> debug : 不报错但结果不对
    debug --> rollback : 定位到配置问题
    rollback --> load : 修正配置

    ok --> [*]

当 DSL 开始承载业务行为,它就已经是治理系统了

我现在对 DSL 的一个核心判断是:它从来不只是一个技术组件。

它表面上像规则系统、表达式系统、配置平台。可一旦它开始承载真实业务,它就一定会带出治理问题:

  • 谁可以写规则
  • 谁有权修改
  • 谁来 review
  • 谁负责解释这条规则的业务含义
  • 谁承担误配的后果
  • 谁来判断这次变更值不值得上
  • 谁来回滚
  • 谁来看影响面

这些问题如果都是糊的,DSL 一定会越用越乱。因为到最后,团队面对的已经不是“怎么写一条规则”,而是“怎么在不把系统搞成黑盒的前提下,让配置去承载业务后果”。

所以我现在评价一个 DSL,最后会落到一个很朴素的标准上:它有没有让团队更敢改。

如果一套 DSL 刚推出来时大家都觉得很方便,用一段时间以后却逐渐变成这样:

  • 改之前总担心误伤别的场景
  • 出问题时没人能快速讲清楚
  • 只有少数人吃得透规则体系
  • 每次修改都像在拆盲盒

那它其实已经开始失败了。

最后留下来的几个判断

这些年看下来,很多 DSL 不是因为团队不够聪明才做坏的,反而常常是因为太聪明、太想一步到位、太想把表达空间做大,最后才把系统做重。

配置当然有价值,DSL 当然也有价值。只是前提得说清楚:当配置开始描述业务行为时,我们其实就在设计一门语言。既然是在设计语言,就不能只盯着“能不能写”,还得同时设计它的边界、解释方式、失败语义和治理方式。

一个好的 DSL,不该让人什么都能表达。它应该让那些确实值得配置化的变化,用一种大家看得懂、出了问题说得清、上线前能拦住明显错误的方式表达出来。