一个规则分发器的演化笔记

0 阅读3分钟

一个规则分发器的演化笔记

一、起点:分支不重叠时,一切都很简单

假设有一段逻辑,根据输入类型做不同处理:

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)
    }
    // ...越来越多
}

问题出现了:

  1. 条件重叠:同一个 input 可能同时满足好几条
  2. 顺序敏感:把某条 if 挪到前面,行为就变了
  3. 越加越乱:每加一种情况,都要重新梳理一遍“谁先谁后”
  4. 无法独立测试:想测某一条分支,得构造整个函数的上下文

这时候,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 // 真正执行
}

分发器只负责按阶段调用,具体每一步做什么,全在规则里。

契约越细,规则越规范,但样板越多;契约越粗,规则越自由,但一致性越弱。按场景权衡,没有标准答案。