规范文档写完发群里,点赞一片,三周后没人再提。半年后复盘发现:提交信息还是随手写,生成代码照样手工改,测试目录越来越像杂物间。问题不在规范内容,在于它从来没有执行入口。
这篇文章只谈一件事:哪些规范值得写、哪些只能靠别的手段,以及检查手段本身什么时候会失效。
1. 规范失效的三个原因
| 原因 | 典型表现 | 根因 |
|---|---|---|
| 写成愿望 | "代码应当清晰、可维护" | 无法判定,谁都可以说自己做到了 |
| 没有执行入口 | 只在 wiki 里,PR 时不出现 | 记住规范的成本由个人承担 |
| 没人检查 | 违规合并后无反馈 | 违反的代价为零,遵守的收益为零 |
三条里最容易修的是第二条。把规范搬到 PR 模板和 CI 里,等于把"记住"换成"被提醒",这一步的收益比写更细的规范大得多。
2. 该规定什么,不该规定什么
判据只有一条:能不能被一台不睡觉的机器判定。
| 值得写进规范 | 不值得写进规范 |
|---|---|
提交信息格式(type(scope): subject) |
"提交信息要有意义" |
目录职责(adapters/ 只放外部集成) |
"代码结构要合理" |
| 生成物必须带标记且不得手工改 | "生成代码要看一遍" |
| 新依赖需在清单里登记 | "少引入依赖" |
敏感文件禁入仓库(.env、密钥、样本) |
"注意安全" |
| PR 必填项(影响面、回滚方式) | "充分自测" |
命名后缀约定(*DTO、*Adapter) |
"命名要准确" |
右列不是没价值,而是价值无法通过文字传递——它们靠评审、靠结对、靠让人看到一次好代码。写进规范只增加字数,不改变行为。
3. 能自动化的检查项
最小可用的门禁只需要五条,跑在 pre-commit 与 CI 两层:
#!/usr/bin/env bash
# scripts/guard.sh — 本地与 CI 共用,返回非零即阻断
set -euo pipefail
# 1) 提交信息格式
msg="$(git log -1 --pretty=%s)"
[[ "$msg" =~ ^(feat|fix|refactor|docs|test|chore|perf)(\([a-z0-9-]+\))?:\ .+ ]] \
|| { echo "提交信息不符合 type(scope): subject"; exit 1; }
# 2) 敏感文件禁入
if git diff --cached --name-only | grep -qE '(^|/)\.env($|\.)|\.pem$|\.p12$|(^|/)secrets/'; then
echo "检测到敏感文件,已阻断"; exit 1
fi
# 3) 生成物必须带标记
for f in $(git diff --cached --name-only -- '*.java' '*.ts' '*.py'); do
grep -qE '@generated|Code generated by|DO NOT EDIT' "$f" \
|| echo "warning: $f 未见生成物标记(如为手写请忽略)"
done
# 4) 依赖引入需登记
git diff --cached --name-only | grep -qE 'pom.xml|package.json|requirements.txt' \
&& { git diff --cached | grep -qE '^\+' && echo "请同步更新 docs/deps.md"; }
CI 侧只做一件事:把同一份脚本再跑一遍。
name: guard
on: [pull_request]
jobs:
guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 2 }
- run: bash scripts/guard.sh
- run: bash scripts/check-generated.sh # 重新生成后 diff 必须为空
第二条 check-generated.sh 是关键补充:生成物只在"重新生成后 diff 为空"时才算可合并,否则说明有人手工改过。
# check-generated.sh 核心断言
pnpm run codegen # 或 mvn generate-sources
git diff --exit-code -- 'generated/**' \
|| { echo "生成物被手工修改,请改模板或源定义"; exit 1; }
4. 不能自动化的部分:写成可判定的问句
架构判断、命名语义、抽象是否多余,这些交给评审。但评审清单的写法决定它有没有用:
| 不可用的写法 | 可判定的写法 |
|---|---|
| "设计是否合理?" | "新增的这层转发,去掉后调用方要改几处?" |
| "命名是否清晰?" | "这个 Manager 类里有没有两个以上不同生命周期的职责?" |
| "有没有考虑异常?" | "外部调用失败时,这个方法的返回值和副作用分别是什么?" |
| "测试是否充分?" | "新增分支里,哪一条没有被任何用例覆盖?" |
放进 PR 模板的形式:
## 评审必答(每项一行,写不出就说明还没想清)
- 影响面:调用这个接口的服务有哪些?失败时它们的表现?
- 回滚:出问题后回到上一版本需要几步?有无数据迁移?
- 例外:本次是否有违反规范之处?若有,例外编号是?
- 不作为:这次刻意没做什么?留给谁、什么时候做?
四问的共同点是答案可以被别人反驳。形容词不行,"几步""哪些""有没有"可以。
5. 机器辅助的边界
检查越细,绕过越自然。这是可预期的博弈,不是道德问题:
| 检查项 | 常见的绕过方式 | 更稳的落点 |
|---|---|---|
| 方法行数 ≤ 50 | 拆成一串只有一个调用的小方法 | 检查认知复杂度上限,不查行数 |
| 圈复杂度 ≤ 10 | 用策略表把分支藏进 map | 查分支覆盖,不查分数 |
| 注释率 ≥ 20% | 灌无信息注释 | 只要求公共 API 有契约说明 |
| TODO 数量 = 0 | 改写成 FIXME 或挪进 issue |
要求 TODO 带责任人与日期 |
| 依赖数不增 | 塞进内部公共库再引入 | 查依赖树的传递引入 |
一条经验:能被一行 if 绕过的规则,最终都会被绕过,而且绕过后更难读。 宁可少两条规则,也别加一条可以便宜绕过的规则。
6. 生成代码的标记与责任归属
标记的作用不是"免责",是让下一个人知道该改哪里:
// @generated by openapi-generator 7.x from api/order.yaml
// DO NOT EDIT. 修改请改 api/order.yaml 后重新生成。
package com.example.order.api;
配套三条约定:
| 约定 | 落地方式 | 违反后果 |
|---|---|---|
| 生成目录只读 | CODEOWNERS 指向生成器维护者 | 评审直接打回 |
| 生成物可重复 | CI 重新生成后 git diff --exit-code |
门禁红灯 |
| 责任归属 | 谁提 PR 谁负责,即使代码是模型写的 | 出问题找 PR 作者 |
第三条要写清楚,因为它的反面很常见:"这是 AI 生成的,我不知道"。责任随提交转移,与生成方式无关。
7. 例外流程
没有例外流程的规范,会被"悄悄违反"替代。
<!-- docs/exceptions/EXC-0007.md -->
- 规则:新依赖需登记在 docs/deps.md
- 原因:临时接入内部日志 SDK,其坐标未公开
- 影响:依赖清单与实际不一致,供应链扫描会漏
- 到期:2025-06-30
- 批准:@owner-a
四个字段缺一不可。到期日是最重要的一栏——例外没有期限,就等于规则被删除,只是没人宣布。
到期的例外要有人看:一个每周跑一次的脚本,扫 docs/exceptions/*.md 里的日期,过期的发到值班频道即可。
grep -H '^- 到期:' docs/exceptions/*.md \
| awk -F':' '$2 < "'"$(date +%F)"'" { print $1 }'
8. 版本与演进
| 项目 | 建议 | 说明 |
|---|---|---|
| 归属人 | 一位,不是委员会 | 多人共管等于无人负责 |
| 存放位置 | 与代码同仓,docs/spec/ |
能跟着 PR 一起改,评审可见 |
| 回顾周期 | 每季度一次,30 分钟 | 只看"哪条规则被绕过最多" |
| 变更方式 | 提 PR 改规范,附一条触发它的真实案例 | 没案例的规则不加 |
| 废弃 | 显式删除并写进变更记录 | 留着不执行的规则会稀释全部规则的分量 |
季度回顾只回答一个问题:过去三个月,哪条规则被绕过最多? 答案要么是规则写错了,要么是该换成检查。两种都值得改。
9. 培训靠三件具体的东西
| 手段 | 具体做法 | 起效时间 |
|---|---|---|
| 新人第一周 | 第一天跑一遍 guard.sh,故意让它拦一次 |
当天 |
| 代码模板 | 脚手架生成的目录、注释头、测试骨架即规范 | 首次提交 |
| 示例仓库 | 一个被评审过的完整 PR,可对着抄 | 第一周内 |
第三条最容易被忽略也最有效:新人不是照规范写代码,是照最近的合并记录写代码。示例仓库过时,规范就等于没有。
10. 度量:看违反率,别看阅读量
| 指标 | 定义 | 为什么用它 |
|---|---|---|
| 规则违反率 | 每个 PR 被拦的规则分布 | 直接指出哪条规则最难遵守 |
| 返工率 | 同一 PR 因规范问题被改几轮 | 衡量规范是否增加摩擦 |
| 例外存量 | 未到期例外数 / 过期未关数 | 过期未关是最真实的失效信号 |
| 首轮通过率 | 一次通过门禁的 PR 占比 | 过低说明规则太严或提示不清 |
不要统计文档阅读量、培训出勤、规范页数。它们只反映投入,不反映行为。
11. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 门禁红灯一片,大家直接 admin merge | 一次上太多规则 | 先上两条,稳定两周再加 |
规则被 # noqa / // ignore 绕过且无记录 |
缺少例外流程 | 要求绕过必须带例外编号 |
| 新代码风格接近老代码 | 示例仓库或模板过时 | 季度同步模板,选一个标杆 PR |
| 规范文档被拆成十几页 | 把评审判断也写进去了 | 一页 checklist + 附录案例 |
| 同一 PR 反复被同一条规则打回 | 提示信息没说怎么改 | 报错里给正确示例,不只给错误 |
最后一条经常被当成小事:提交信息不符合规范 这句话让人去翻文档,示例:fix(order): 修正退款金额取整 让人直接改。
12. 边界
| 场景 | 建议 | 理由 |
|---|---|---|
| 团队少于五人 | 不写规范,只约定提交格式 | 口头沟通成本低于文档维护成本 |
| 规范超过两页 | 拆成一页 checklist | 读不完等于不存在 |
| 已有强 CI 纪律 | 只补缺口项 | 增量价值有限,重复建设会稀释信任 |
| 短期外包项目 | 只保留敏感文件与依赖两条 | 交付后没人维护 |
| 纯研究原型 | 不设门禁 | 探索期规则会抑制试错 |
| 已有成熟团队习惯 | 先观察,别先规定 | 把既有做法写下来比引入新规则便宜 |
规范的本质是降低协作摩擦,不是提高代码质量上限。质量上限靠评审和人,规范负责的是"不要每次都重新争论同一件事"。
13. 自测
- 判断一条规则该不该写进规范,用什么单一判据?
- 为什么"方法行数 ≤ 50"这类规则容易被绕过,且绕过后更糟?
- 生成物"可合并"的判定条件是什么?
- 例外记录里最不能省的一栏是哪个,为什么?
- 季度回顾只需要回答哪一个问题?
- 为什么"文档阅读量"不适合作为规范落地的度量?