附录 A — 规则全量表
二十三条架构规则的全量清单,排序不按字母,按”值得先抄哪条”。 需要先说明的是:这张表的正确用法不是从上往下抄, 小节 A.12 会给出用法,而中间这几节解释的是 为什么这些规则长成了现在这个样子—— 那部分比清单本身可迁移得多。
A.1 先看一个统计
按实现方式分,二十三条规则分成两类:九条是文本模式匹配, 十四条是语义分析,而每一类语义规则各自是一个独立的分析器 —— 服务查找归属、访问器契约、可选用法契约、兜底契约、目标分离、 单例绕过、界面目标粒度、本地化表归属、依赖规则、注释代码、 文件健康度、测试宿主契约,等等。
超过一半的规则不是”grep 一个字符串”,而这个比例本身就是一条信息: 当你认真对付一类问题时,文本匹配很快就不够用了 —— 小节 13.5 里那个五层过滤的例子说明了这个过程是怎么发生的。 档位上的分布是二十条拦截、三条报数。
A.2 第一梯队:任何团队都该有,零基建
这五条不依赖任何构建系统,用一个脚本加一份配置就能实现。 它们的共同点是判据都落在单个文件甚至单行文本上 —— 不需要知道这个文件属于哪一层,也不需要知道谁依赖谁。
| 规则 | 它守什么 | 换来的可测性 |
|---|---|---|
| 无条件跳过检测 | 跳过语句必须有真实的运行时理由 | 覆盖不会被静默删除 |
| 注释掉的代码 | 代码即唯一事实源,历史归版本控制 | 读代码时不必判断哪段是活的 |
| 单一日志 owner | 日志只在一个文件里定义 | 日志配置有唯一入口,可改可测 |
| 文件健康度 | 按变更原因拆,行数是启发式上限 | 单个文件的变更原因唯一 |
| 重试点击辅助函数 | 禁止”点到成功为止” | 一次意图只提交一次 |
第一条和第五条是专门用来防”测试看起来通过了”的(小节 12.10), 而它们值得排在最前面,因为假绿比没有测试更危险—— 没有测试的时候你知道自己没有保护,而假绿会让你以为自己有。
A.3 第二梯队:有共享层之后
这一梯队需要一个”共享层 / 产品层”的划分,但不需要构建图。 也就是说,你得能从路径判断一个文件属于哪一边, 而这个能力靠目录约定就能提供(小节 6.14.1)。
| 规则 | 它守什么 | 换来的可测性 |
|---|---|---|
| 协议优先查找 | 只在协议层顶层做服务查找 | 契约不被绕过 |
| 可选访问器契约 | 返回可选的访问器不许在未注册时崩 | 调用方的可选分支是真的 |
| 禁止服务兜底 | 没有不经过正常装配的隐藏路径 | 行为唯一,可断言 |
| 协议与实现目标分离 | 契约和实现不编在同一个目标 | 分层不是伪边界 |
| 具体单例绕过 | 消费者不许直接用具体实现的单例 | 生命周期归微内核 |
| 产品互不依赖 | 跨产品复用必须下沉共享层 | 产品可独立构建与测试 |
| 本地化表归属 | 只有一个消费者的文案不放共享表 | 改一句文案不动全产品 |
| 权益单一 owner | 权益查询收口到一个服务 | 不存在第二个真相源 |
“禁止服务兜底”是这一梯队里最值钱的一条,而它也是实现最复杂的一条 (小节 13.5)—— 这两件事不是巧合: 一条规则的价值和它的实现难度经常正相关, 因为容易表达的约束通常也是容易遵守的约束。
A.4 第三梯队:需要构建依赖图
第三梯队的六条各自守着构建图上的一条性质: 界面目标粒度守的是页面和组件按功能拆成独立目标; 部署件归属守的是兄弟部署件不许直连、只经共享层; 测试宿主契约守的是有界面测试就必须有可执行宿主; 测试 bundle 经包装器守的是名字由包装器推出、不由调用方起; 发布走账本守的是发布流水线不许绕过账本直接触发; 构建工具不依赖客户端守的是构建期工具与客户端图隔离。
这六条的共同前提是你的构建系统能被查询—— 如果你的构建是一堆脚本,这六条一条都实现不了, 而那本身就是一个值得先解决的问题。
A.5 三条报数模式的
| 规则 | 现状 | 为什么还没拦 |
|---|---|---|
| 本地化表归属 | 966 处违规 | 存量太大,正在收敛 |
| 产品互不依赖 | 零违规 | 已经干净,随时可切 |
| 音频会话单一写者 | 零违规 | 产品域规则,观察期 |
第二行和第三行值得注意:它们已经零违规了,却还在报数模式。 这说明一件事 —— 从”零违规”到”切成拦截”之间还差一个决定, 而这个决定没有 owner(小节 15.7)。 小节 A.10 会算清楚这个状态到底损失了什么。
A.6 每条规则必须有的四个字段
不管是哪一类,四个字段是强制的,而每一个都对应一种具体的失效。 incident 记的是产生它的那次失败,缺了它半年后没人能判断它该不该留; fix_hint 记的是怎么修,缺了它 Agent 会换个写法再撞一次; sentinel_min 记的是至少应该看到多少个事实, 缺了它规则坏了会表现成通过;档位记的是拦截还是报数, 缺了它上线即停摆。
第三个字段在实现上是必填且类型上不能为零(小节 13.4.3), 也就是说你没法写一条不带自检的规则。 这四个字段合起来其实是一个很小的要求 —— 写一条规则多花五分钟 —— 而它们防住的是这一章末尾那张表里列的七种死法。
A.7 语义规则和文本规则的分界线在哪
十四条语义规则、九条文本规则,这个分界线不是随意的, 它有一个清楚的判据:这条规则要判断的东西, 能不能从单行文本读出来?能就是文本模式够用,不能就需要语义分析。
三个例子摆在一起最清楚。禁止内联日志器只需要知道 “这一行有没有那个构造调用”,单行够;禁止无条件跳过只需要知道 “这一行的参数是不是常量”,单行够;而禁止服务兜底需要知道 “这个 ?? 左边的值是从哪来的”—— 单行不够。
第三条实际上需要回答四个问题:这个变量是不是从一个已知可选的访问器来的? 它有没有被局部变量遮蔽?这个 ?? 是不是在同一条语句里? 右值是不是那个被明确允许的”不可用”?四个问题, 全都超出了单行的范围,而这就是那个五层过滤器存在的理由。
这个分界线有一个很实用的推论:当你发现自己在给一条文本规则 加第三个例外时,它可能已经该变成语义规则了。 因为例外的数量正是”单行信息不够”的症状 —— 每一个例外都是在用一个笨拙的方式补充上下文, 而当你补到第三次的时候,直接去拿真正的上下文会更便宜。
A.8 规则的可移植性
这张表里有多少条能直接搬到别的仓库?粗估一下: 无条件跳过、注释代码、文件健康度这三条可移植性高, 概念和实现都通用;单一日志 owner 和协议优先中等, 概念通用但实现绑定语言;服务兜底和访问器契约低, 绑定这套微内核架构;目标粒度和部署件归属也低,绑定构建系统。
高可移植的只有三到四条。 这个估计比”抄这二十三条”有用得多, 因为它把注意力引向了真正可移植的东西 —— 不是规则,是方法。
方法有五条:怎么发现该有哪条规则(小节 15.5)、 怎么调它的边界(小节 14.4)、 怎么让它上线而不停摆(小节 15.3)、 怎么让它自检(小节 13.4.3)、 怎么判断它该退休(小节 15.7)。 这五条方法,可移植性是百分之百—— 它们不依赖语言、不依赖构建系统、也不依赖仓库规模。
A.9 二十三条规则的分布说明了什么
把这二十三条按”它守什么”归类,会看到一个不均匀的分布: 八条守单一 owner 与写入权,四条守测试的可信度, 四条守分层与依赖方向,三条守目标粒度与资源, 两条守代码即事实源,两条是产品域特定的。
第一类占了三分之一以上。 而加上路径不变量那二十份里的九份 (小节 14.5),整个规则体系里 超过 40% 在回答同一个问题:这块状态归谁写。
这个分布不是设计出来的,是撞出来的(小节 15.11)—— 也就是说,在这个系统撞过的所有墙里, “同一份状态有两个写者”占了将近一半。 这个数字给形状 B(小节 4.3)提供了一个量化的支撑, 而它对读者的用处很直接:如果你只能防一个形状,防这个。
A.9.1 第二类也值得注意
四条规则守的是”测试的可信度”,而不是”测试的覆盖”—— 无条件跳过、重试点击辅助函数、界面测试必须有可执行宿主、 测试 bundle 经包装器。没有一条是”必须写测试” 或”覆盖率必须达标”。
这个选择很能说明这套系统的判断:覆盖率这类指标 可以用别的方式保证(流程、评审、习惯), 而”绿灯可不可信”必须有独立的守卫。 理由在失效的可见性上 —— 覆盖率失效时会被发现,因为数字会掉; 而绿灯的可信度失效时不会(小节 12.3), 它表现为一片安静的绿色。
A.10 “值不值”怎么算
和路径清单那个算法(小节 14.14)类似,但变量不同: 收益约等于违规发生的频率乘以每次违规漏进主干的代价, 成本约等于实现加调参加持续的误报打断。
这个算式给出了一个反直觉的结论:一条从来不报违规的规则, 收益是零。 不管它守的东西多重要 —— 如果没有人试图违反它, 那么它没有拦住任何东西。
这就是 小节 A.5 里那两条零违规的报数规则 处境尴尬的原因:它们既不拦人(无保护),又没有违规(无收益), 只是每次多跑一遍扫描。正确的处理不是删掉它们 —— 因为”现在没人违反”不等于”以后没人违反”—— 正确的处理是切成拦截:成本仍然接近零(不会误报,因为没有违规), 而它从此提供了保护。
一条零违规的报数规则,是唯一一种可以零成本切成拦截的规则。 这套系统里有两条这样的规则,都还没切 —— 这是那个”没有 owner 的决定”最具体的表现。
A.11 三个梯队之间的顺序为什么不能跳
三个梯队按基建要求排,而它们同时也是一个推荐的实施顺序 —— 理由不只是”先做便宜的”。
第一梯队守的是判定本身的可信度(小节 12.10)。 在这五条建成之前,后面任何一条规则的绿色都是没有意义的: 你不知道那条规则是真的没有违规,还是它的扫描面已经缩到了零。 先建可信度,再建覆盖面—— 这和 小节 16.14.4 是同一条,只不过那里说的是检查, 这里说的是规则。
第二梯队守的是分层,而它有一个前提: 你的仓库里得先有一条清晰的层。 一条”产品互不依赖”的规则,在一个还没有区分产品和共享层的仓库里 写不出来 —— 不是难写,是没有一个可靠的方式判断 一个文件属于哪一层(小节 6.14.1)。 所以第二梯队的真正前提不是技术,是结构。
第三梯队守的是构建图上的性质,前提最硬: 你的构建系统得能被查询。这一条是全书唯一一处 可能需要重基建的地方(小节 10.16), 所以它排在最后不是因为它不重要, 是因为它是唯一一个可以正当地被无限期推迟的梯队。
三个梯队的顺序还有一层含义:它们的规则数量应该是递减的。 如果你的第三梯队比第一梯队条数还多, 那通常说明规则集是从一个”我们的构建系统能查什么”出发写的, 而不是从”我们撞过什么”出发写的 —— 而前者正是 小节 15.11 那张表里误报率最高的那一类。
A.12 怎么用这张表
不要从上往下抄。 正确的用法是读一遍,然后合上书, 写下你团队在评审里重复说得最多的那三句话。 如果其中有一条和这张表里的某条重合, 那说明你已经”挣得”了那条规则,可以直接开始实现。
如果一条都不重合,那就实现你自己那三条,别管这张表 —— 小节 15.5 讲过为什么。 这张表最大的用处不是提供答案,是提供一个对照: 让你看到一套长了半年多的规则集大概长什么样、 密度是多少、分布是什么形状。
A.13 规则自查表
对你自己的每一条规则,逐项打勾:
□ 我能说出它诞生于哪次具体的失败
□ 它有 incident 字段(或等价的"为什么有这条")
□ 它有 fix_hint 字段(或等价的"怎么修")
□ 它有哨兵下限,且哨兵不能被关掉
□ 它上线时走了报数模式
□ 它的误报率我知道是多少
□ 它有一个有名字的 owner
□ 我知道它变成结构之后会是什么样
八项,而大部分规则集能打勾的不超过三项。 但这八项的价值不在于”全打勾才合格”—— 在于每一个没打勾的项,都对应一种具体的死法: 说不出诞生原因,半年后没人能判断它该不该留; 没有 fix_hint,Agent 换个写法再撞一次; 没有哨兵,规则坏了会表现成通过; 没走报数模式,上线即停摆然后被关掉; 不知道误报率,被绕过而你不知道; 没有 owner,没人判断它该不该退休; 不知道结构化的终点,规则集单调增长。
七种死法,而 小节 15.9 讲了其中三种的检测方式—— 剩下四种目前只能靠人定期去看,而这本身就是一个待补的缺口。