8.5 KiB
8.5 KiB
03 · 数据权威 · 错误处理 · 演进
本篇是正确性的地基:数据从哪来、错了怎么暴露、代码怎么随规则长大而不腐化;数据权威部分在既有「数据权威原则」上扩展到前后端全景。
1. 数据权威(前后端全景)
1.1 四条铁律(承接数据权威原则)
- 数据源唯一:同一业务数据只有一个权威来源;上游算/写/校验,下游只读/消费,不重复推断、不重复拼装。
- 禁止猜测兜底:不用默认值掩盖缺失。关键权威字段缺失要显式报错或返回
null,由上层决定是否终止; 禁止|| 0、|| []、|| ''、双源回退等隐式掩盖。 - 下游不修补:下游不得为"让流程跑起来"而补写/重算上游本应提供的数据;发现下游在修补,把责任前移到权威源。
- 边界内可默认:只有展示层、纯 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. 架构审查清单
- SSOT:这段数据有没有第二处在算?下游是不是只读?
- 依赖方向:有没有下层依赖上层 / 环依赖 / 入口层写算法?
- 职责边界:这段逻辑属于本模块吗?是不是重造了别处的能力?
- 关注点分离:决策与算法、数据与表现分开了吗?
- 扩展 vs 修改:新增能力是"加+注册"还是"改核心"?该用扩展点吗?
- 不过度设计:这个抽象有 ≥2 个真实变体吗?还是想象的?
- 配置化:有会变/复用/无语义的裸值该外提吗?配置只解析一次吗?
- 显式失败:关键路径缺数据是报错还是兜底掩盖?
- 错误处理:有没有空 catch / 吞异常 / 带病继续?
- 可观测:关键节点有可复盘日志吗?诊断探针清了吗?
- 演进:有没有留下第二份并行实现 / 死代码没区分意图?
6. 本篇小结
- 数据权威四铁律 + 前后端权威划分:服务端裁定、前端表现、shared 同源不转移权威。
- 错误处理:fail-fast、不吞异常、分层归属、可观测、诊断即清。
- 演进:收敛并行实现、扩展点先行、区分死代码与扩展点、一次一提交。
- 用反模式清单和审查清单把三篇的原则落到每次改动上。
回到 README 查看导航与一页纸总则。