Frontend / Full Stack SWE · Coding

Chatbot Refactor:命令边界、Handler注册与状态隔离

先保留可观察行为,再把解析、路由和存储拆开。

题目重点:先保留可观察行为,再把解析、路由和存储拆开。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 未知starter/test无法复原。训练支持大小写敏感/meet、/taco、/away,以完整首token匹配;未知命令作为普通文本。handler由外部注入,不臆造每个命令的业务含义。
  • 输入id/text,重复同id同text返回已有record,不同text拒绝;同步纯handler返回可JSON序列化值。内存保存独立副本,无多进程持久化/外部发送。

解题路线

  1. 重构前先把已有输入输出变成characterization tests:合法命令、空参数、普通消息、异常和原存储语义。不先删旧函数或把需求改成个人偏好,否则无从证明行为保持。
  2. parse仅识别命令与参数,registry将命令映射handler,storage按message ID存record。解析不负责发通知/改用户状态,handler也不负责修改dispatcher历史,职责清楚才容易加新bot。
  3. 匹配命令边界而非startsWith:/meeting不能落到/meet。具体是否支持前导空格、大小写、quoted参数必须按合同,训练首字符必须/,参数只去掉命令后的分隔空白。
  4. handler完成且输出可序列化后才存record;异常无半成品。读/返回深副本避免调用方篡改内部数据。JSON复制限JSON值,不支持Date/BigInt/cycle等;要求这些类型时需重新选择copy协议。
  5. 此处纯handler使本地回滚简单,真实bot外部副作用不能靠map回滚;需幂等message/action ID、outbox和重试状态。同步handler若变异步还要同id在途请求合并,不能等处理结束才首次去重。

手动走一遍

/meet alice调用meet handler,/meeting alice当普通消息;两者prefix相近但完整token不同。handler抛错,不存message;调用方修改返回output,read仍原内容。独立首token扫描、失败rollback、重复1000次只调用一次handler共1147项核验。

JavaScript 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

const cloneJson = value => JSON.parse(JSON.stringify(value));
export class Dispatcher {
  constructor(handlers, ordinary = text => ({ text })) {
    this.handlers = new Map(Object.entries(handlers));
    if ([...this.handlers].some(([command, handler]) => !/^\/[a-z]+$/.test(command) || typeof handler !== 'function')) throw new Error('handler registry');
    if (typeof ordinary !== 'function') throw new Error('ordinary handler');
    this.ordinary = ordinary;
    this.messages = new Map();
  }
  process({ id, text }) {
    if (typeof id !== 'string' || !id || typeof text !== 'string') throw new Error('message');
    const previous = this.messages.get(id);
    if (previous) {
      if (previous.text !== text) throw new Error('id reused with different text');
      return cloneJson(previous);
    }
    const match = /^(\/[^\s]+)(?:\s+([\s\S]*))?$/.exec(text);
    const command = match?.[1] ?? null;
    const handler = this.handlers.get(command);
    const output = handler ? handler(match[2] ?? '') : this.ordinary(text);
    if (output && typeof output.then === 'function') throw new Error('synchronous handlers only');
    const record = { id, text, command: handler ? command : null, output: cloneJson(output) };
    // A failed handler/clone never leaves half a stored record.
    this.messages.set(id, record);
    return cloneJson(record);
  }
  read(id) { return this.messages.has(id) ? cloneJson(this.messages.get(id)) : null; }
}

复杂度

解析O(text length),map查/存平均O(1),JSON复制O(output/record bytes);存M消息O(总bytes),无限保留会增长,生产需TTL/持久记录。handler业务成本另计。

易错点与追问

容易失分的边界

  • refactor时删旧代码导致无法比较test行为。
  • 将复杂多bot条件换成registry却改变命令匹配语义。
  • 返回内部record引用使后续内容被外部修改。

继续深挖

  • 异步handler和同message并发到达,怎样避免双执行?
  • 需要事务修改多个bot状态时,命令执行与事件存储如何协调?
✓已复制微信号!