Files
erqiwang_youle/docs/games/engineering/03-数据权威·错误处理·演进.md
T

8.6 KiB
Raw Blame History

03 · 数据权威 · 错误处理 · 演进

本篇是正确性的地基:数据从哪来、错了怎么暴露、代码怎么随规则长大而不腐化。 数据权威部分在 .github/copilot/skills/data-authority-principle.md 基础上,扩展到前后端全景。


1. 数据权威(前后端全景)

1.1 四条铁律(承接数据权威原则)

  1. 数据源唯一:同一业务数据只有一个权威来源;上游算/写/校验,下游只读/消费,不重复推断、不重复拼装。
  2. 禁止猜测兜底:不用默认值掩盖缺失。关键权威字段缺失要显式报错或返回 null,由上层决定是否终止; 禁止 || 0、|| []、|| ''、双源回退等隐式掩盖。
  3. 下游不修补:下游不得为"让流程跑起来"而补写/重算上游本应提供的数据;发现下游在修补,把责任前移到权威源。
  4. 边界内可默认:只有展示层、纯 UI 兼容、非关键日志字段,才可在明确边界内用安全默认值。

1.2 前后端的权威划分

        权威裁定                             表现/预判
┌──────────────────────┐        ┌──────────────────────┐
│ 服务端 = 真相之源      │  推送   │ 前端 = 从推送重建表现   │
│ 房卡/胜负/积分/发牌/   │ ─────▶ │ 只显示、可预校验,      │
│ 手牌/状态/结算…全权威   │        │ 不"前端说了算"          │
└──────────────────────┘        └──────────────────────┘
         ▲  同一套 shared 纯逻辑(逐字相同)在两端各跑一份  ▲
         └──────────────────────────────────────────────┘
  • 服务端权威:一切影响胜负/计分/状态的判定以服务端为准;前端结果只是表现与预判。
  • 逻辑同源 ≠ 权威转移:前后端共享同一套算法(shared)是为了表现一致 + 预校验, 最终仍以服务端算的为准(见各端 development-guide 的 shared 说明)。
  • 一份数据一个方向:产生 → 推送 → 消费,单向、可追踪;前端不把"预判结果"当权威回写。

1.3 审查信号

看到 || []、|| 0、|| ''、三元默认、双源字段——先判断它是不是权威字段(影响发牌/庄家/ 手牌/规则/结算/重连/状态流转):

  • 是 → 改成权威读取(缺失显式失败)。
  • 只是展示/日志/统计 → 才可保留默认值。

2. 错误处理与可观测性

2.1 fail-fast:把错误暴露在最近处

  • 关键路径缺前置条件(缺权威数据、状态非法),立即中止并给出明确错误,别带病继续。
  • 不吞异常:try/catch 不是用来"让它别报错",而是用来在正确的边界处理并记录。空 catch{} 等于把故障藏起来,是重大反模式。
  • 校验前置:入口处集中校验参数/状态,不合法即返回;业务逻辑内部可假设前置条件已满足。

2.2 错误的分层归属

层 出错时怎么办
入口/收发层 参数/身份/状态校验失败 → 结束请求(对外不泄漏细节,见 development-guide 的"静默 return")
编排层 前置条件不满足 → 显式失败,不替下层补数据
领域/算法层 输入非法 → 抛错/返回 null,不猜一个"看起来对"的结果
数据层 权威字段缺失 → 显式失败,暴露数据链断点
表现层 可容错降级(缺图/缺文案用占位),不影响业务判断

2.3 可观测性

  • 分级日志:关键节点(收包、状态转换、结算、扩展点命中)留可复盘的日志;日志说清上下文 (房间、座位、阶段),不是一句 "error"。
  • 可复盘:保留收包/关键中间态记录(如调试保存收包),出问题能重放。
  • 诊断探针即用即清:为定位问题临时加的日志/探针,定位完成后必须删除,不留在正式代码里。
  • 正式代码不为测试服务:不为"让测试过"在正式代码加 fallback/守卫/冗余字段;测试构造符合 生产契约的输入与 stub(详见 development-guide 的测试纪律)。

3. 演进与重构纪律

代码会随规则长大。让它长而不腐的关键,是持续把"并行/重复/临时"收敛掉。

3.1 收敛并行实现

  • 同一职能出现第二份实现(哪怕是"简化版/兼容版"),就是分叉的开始——尽快收敛为一份权威。
  • 收敛方向:把旁路实现删掉,改为调用权威模块;权威能力不足则在权威模块内扩展。

3.2 扩展点先行,而非到处开分支

  • 需要支持新变体时,优先看有没有现成扩展点(策略注册/管线节点/工厂分派);有则加一项。
  • 没有且已达"三次法则"→ 重构出扩展点,再加变体;不要在核心里再堆一个 if。

3.3 死代码与"有意保留的扩展点"要区分

  • 死代码(不可达、被替代、幻觉引用)→ 删除,减少认知负担。
  • 有意保留的扩展点(当前不可达但为将来能力预留)→ 注释写明意图,避免被当死代码清理, 也避免被误当"已实现"。二者的区别必须在代码里显性表达。

3.4 改动的提交纪律

  • 一次提交聚焦一件事(一个修复/一个功能/一处重构/一批测试),信息写清"做了什么/为什么"。
  • 业务缺陷与测试缺陷分开修、分开提交;重构与功能改动不混在一个提交里。

4. 反模式清单(一眼识别)

反模式 为什么坏 正解
隐式兜底 || 0 / || [] 掩盖缺失 把 bug 藏到远处才爆发 权威字段缺失显式失败/返回 null
同一数据多处各算一遍 必然分叉、互相矛盾 单一权威源,下游只读
下游修补上游数据 责任错位,掩盖真问题 责任前移到权威源
入口/表现层里写算法 职责错层,难测难复用 下沉到领域/状态层
一个入口 switch(action) 二次路由 绕开分层、膨胀成上帝函数 一操作一入口/一 rpc 一处理器
空 catch{} 吞异常 故障隐形 在正确边界处理并记录
为"将来也许"预埋抽象 过度设计、徒增复杂 YAGNI + 三次法则,就近演进
魔法数字/字符串散落 改规则要全局翻找 外提为语义化常量/配置
多处各自解析同一配置编码 解析不一致 解析一次 → 只读消费
同职能第二份"简化实现" 并行逻辑分叉 收敛为一份权威,调用不重造
正式代码为测试加 fallback 本末倒置 测试适配生产契约
诊断日志/探针遗留 噪声与信息泄漏 定位后即清

5. 架构审查清单

提交/评审前逐条自检(与 01/02 对应):

  • SSOT:这段数据有没有第二处在算?下游是不是只读?
  • 依赖方向:有没有下层依赖上层 / 环依赖 / 入口层写算法?
  • 职责边界:这段逻辑属于本模块吗?是不是重造了别处的能力?
  • 关注点分离:决策与算法、数据与表现分开了吗?
  • 扩展 vs 修改:新增能力是"加+注册"还是"改核心"?该用扩展点吗?
  • 不过度设计:这个抽象有 ≥2 个真实变体吗?还是想象的?
  • 配置化:有会变/复用/无语义的裸值该外提吗?配置只解析一次吗?
  • 显式失败:关键路径缺数据是报错还是兜底掩盖?
  • 错误处理:有没有空 catch / 吞异常 / 带病继续?
  • 可观测:关键节点有可复盘日志吗?诊断探针清了吗?
  • 演进:有没有留下第二份并行实现 / 死代码没区分意图?

6. 本篇小结

  • 数据权威四铁律 + 前后端权威划分:服务端裁定、前端表现、shared 同源不转移权威。
  • 错误处理:fail-fast、不吞异常、分层归属、可观测、诊断即清。
  • 演进:收敛并行实现、扩展点先行、区分死代码与扩展点、一次一提交。
  • 用反模式清单和审查清单把三篇的原则落到每次改动上。

回到 README 查看导航与一页纸总则。