一个规则分发器的演化笔记
一、起点:分支不重叠时,一切都很简单
假设有一段逻辑,根据输入类型做不同处理:
function handle(input) {
if (input.type === 'A') {
return handleA(input)
}
if (input.type === 'B') {
return handleB(input)
}
if (input.type === 'C') {
return handleC(input)
}
}
这段代码没有任何问题。
- 条件互斥:
type只有一个值,A、B、C 不会同时成立 - 顺序无关:不管谁先判断,结果都一样
- 加一种类型,加一个
if就行 - 每个分支逻辑清晰,测试也好写
如果所有分支都是这样,就根本不需要往下看了。
二、问题出现在:条件开始重叠
现实很快变得不这么整齐。比如:
- 有时要按“类型”处理
- 有时要按“优先级”处理
- 有时要按“用户角色”处理
- 有时两个条件同时满足,还要决定谁优先
代码开始变成这样:
function handle(input) {
if (input.type === 'A' && input.priority > 0) {
return handleA(input)
}
if (input.type === 'A' && input.user.role === 'admin') {
return handleA2(input)
}
if (input.type === 'A') {
return handleANormal(input)
}
if (input.type === 'B' || input.user.role === 'admin') {
return handleB(input)
}
// ...越来越多
}
问题出现了:
- 条件重叠:同一个
input可能同时满足好几条 - 顺序敏感:把某条
if挪到前面,行为就变了 - 越加越乱:每加一种情况,都要重新梳理一遍“谁先谁后”
- 无法独立测试:想测某一条分支,得构造整个函数的上下文
这时候,if/else 已经撑不住了。
三、第一次尝试:给每条分支一个“名字”
先做一个最自然的动作——把每条分支的逻辑抽成独立函数,给它一个名字:
const handlers = {
A: (input) => { /* ... */ },
A2: (input) => { /* ... */ },
ANormal: (input) => { /* ... */ },
B: (input) => { /* ... */ },
}
然后调用方判断该用哪个:
function handle(input) {
let key = null
if (input.type === 'A' && input.priority > 0) key = 'A'
else if (input.type === 'A' && input.user.role === 'admin') key = 'A2'
else if (input.type === 'A') key = 'ANormal'
else if (input.type === 'B' || input.user.role === 'admin') key = 'B'
return handlers[key]?.(input)
}
这就是策略模式。
好处:
- 每个做法独立了,可以单独测试
handlers是一个平级查表
坏处:
- 判断逻辑还是堆在调用方,只是从
if体变成了key计算 - 条件重叠时,还得小心翼翼地排列顺序
- 加一种情况,还是得改
handle
选择逻辑只是换了位置,还没有消失。
四、第二次尝试:把“判断”下放给每条分支
换个思路:为什么判断逻辑一定要写在调用方?
每条分支自己最清楚“我该不该接”。让它自己说。
const rules = [
{
name: 'A',
canHandle: (input) => input.type === 'A' && input.priority > 0,
run: (input) => { /* ... */ },
},
{
name: 'A2',
canHandle: (input) => input.type === 'A' && input.user.role === 'admin',
run: (input) => { /* ... */ },
},
{
name: 'ANormal',
canHandle: (input) => input.type === 'A',
run: (input) => { /* ... */ },
},
{
name: 'B',
canHandle: (input) => input.type === 'B' || input.user.role === 'admin',
run: (input) => { /* ... */ },
},
]
调用方变成:
function handle(input) {
for (const rule of rules) {
if (rule.canHandle(input)) {
return rule.run(input)
}
}
}
调用方不再知道“有哪些情况”“谁先谁后”。它只管把输入丢出去,谁接就是谁。
这就是责任链的一种简化实现:有序规则列表,命中即停。
五、把分发逻辑抽成一个架子
现在“遍历 + 判断 + 执行”这三步,每个场景都要写一遍。把它抽出来:
const NO_MATCH = Symbol('NO_MATCH')
function createDispatcher(rules) {
return function dispatch(input) {
for (const rule of rules) {
if (rule.canHandle(input)) {
return rule.run(input)
}
}
return NO_MATCH
}
}
使用:
const handle = createDispatcher([
ruleA, ruleA2, ruleANormal, ruleB,
])
const result = handle(input)
if (result === NO_MATCH) {
// 没有规则命中
}
分发器内部没有任何业务逻辑:
- 不知道
input是什么 - 不知道有哪些条件
- 不知道每条规则做什么
- 只负责“按顺序试,谁接谁上”
结构统一,逻辑外置。
六、契约:规则要长什么样
分发器不关心业务,但必须约定规则的接口。最简单的契约:
{
canHandle: (input) => boolean,
run: (input) => any,
}
如果逻辑复杂,可以再分阶段:
{
parse: (input) => data, // 把原始输入转成内部结构
canHandle: (data) => boolean, // 判断接不接
prepare: (data) => params, // 生成执行参数
run: ({ params, data }) => any // 真正执行
}
分发器只负责按阶段调用,具体每一步做什么,全在规则里。
契约越细,规则越规范,但样板越多;契约越粗,规则越自由,但一致性越弱。按场景权衡,没有标准答案。