软件项目开发全流程 · 第 16 课 · 编码规范与代码质量
1、一场关于"大括号放哪"的战争
团队里两个人吵了半天:大括号是 if (x) { 还是 if (x)\n{。争论消耗的时间、产生的情绪,远超这件事本身的价值。
风格问题不该由人吵,该由工具定。 这就是编码规范(Coding Standard)与静态检查(Static Analysis)要解决的问题。
本课解决三件事:
- 编码规范到底规范什么?(不只是风格)
- 怎么用自动化工具强制执行,而不是靠自觉?
- 静态检查能帮我们在运行前抓出哪些 Bug?
2、编码规范规范什么:四个层次
| 层次 | 内容 | 例子 |
|---|---|---|
| 格式(Formatting) | 空格、缩进、换行、行宽 | 2 空格还是 4 空格;行宽 100 |
| 命名(Naming) | 变量/函数/类的命名风格 | snake_case vs camelCase |
| 结构(Structure) | 文件组织、函数长度、模块边界 | 一个函数不超过 50 行 |
| 实践(Practice) | 禁止的做法、必须的做法 | 禁止裸 except;必须处理错误 |
前两层(格式/命名)应该 100% 交给工具自动处理——人对这些没有讨论价值。
后两层(结构/实践)才需要人做决策,但也能用 Linter 兜底提醒。
一个反常识的建议:与其争论"哪种风格更好",不如随便选一种(或直接用社区标准,如 PEP 8、Google Style),然后用工具锁定,全团队统一。统一的普通标准,胜过不统一的最优标准。
3、工具链:格式化器 + Linter + 类型检查
三类工具各司其职:
① 格式化器(Formatter):只管排版,不管对错
特点:确定性的、无争议的——同一个输入永远产出同一个输出。团队不再需要为格式评审。
| 语言 | 常用格式化器 |
|---|---|
| Python | black、ruff format |
| JavaScript/TS | prettier |
| Go | gofmt(Go 自带,这是 Go 社区极少有格式争论的原因) |
| Rust | rustfmt |
| Java | google-java-format |
用法:配置一次,保存时自动格式化,CI 里检查"是否已格式化",未格式化直接失败。
② Linter:找"可疑的代码"
Linter 找的是"可能有问题"的模式,比格式化器聪明,但有些规则有主观性。
| 语言 | 常用 Linter |
|---|---|
| Python | ruff、flake8、pylint |
| JavaScript/TS | eslint |
| Go | golangci-lint |
| Rust | clippy |
Linter 能抓的典型问题:
# 未使用的变量
def calc(a, b):
unused = a * 2 # ⚠️ unused 从未被使用
return a + b
# 裸 except,会吞掉所有异常(包括 Ctrl+C!)
try:
do_something()
except: # ⚠️ 太宽泛,应该指定异常类型
pass
# 可变默认参数(Python 经典陷阱)
def add_item(item, bucket=[]): # ⚠️ bucket 在所有调用间共享!
bucket.append(item)
return bucket
# 比较用 == 而不是 is None
if x == None: # ⚠️ 应写 x is None
...③ 类型检查(Type Checker):在运行前发现类型错误
给动态语言加上"编译期"保护,第 21 课详解。这里先知道有这类工具:
| 语言 | 工具 |
|---|---|
| Python | mypy、pyright |
| TypeScript | tsc(自带) |
| Java/C# | 编译器自带静态类型 |
def greet(name: str) -> str:
return "Hello, " + name
greet(42) # mypy 会在运行前就报错:参数类型不符4、静态检查 vs 动态检查
| 静态检查 | 动态检查(运行时) | |
|---|---|---|
| 何时 | 写代码时 / 提交前 / CI | 程序运行时 |
| 看什么 | 代码本身(不运行) | 实际执行行为 |
| 优点 | 快、能在提交前拦住问题 | 能发现真实运行才能暴露的问题 |
| 缺点 | 可能误报、抓不到运行时逻辑错 | 事后才发现,可能已在生产 |
| 例子 | Linter、类型检查 | 单元测试、断言 |
两者互补,都要有:静态检查拦住"低级错误",测试(第 19 课起)验证"业务正确"。
静态检查的核心价值:把问题左移(Shift Left)。
成本
▲
│ ● 生产环境发现
│ ● 测试阶段
│ ● 代码评审
│ ● 提交时 / IDE 实时
│ ●
└──────────────────────────────────────────▶ 开发进程同一个 bug,越早发现,修复成本越低——在生产环境发现一个 bug,成本可能是写代码时发现的 100 倍。静态检查就是在最左边设卡。
5、怎么落地:三道防线
规范不能只写在 Wiki 里,要靠三道防线强制执行:
第一道:IDE 实时 保存时自动格式化 + 实时高亮
↓
第二道:提交前钩子 git pre-commit hook,不合格不让提交
↓
第三道:CI 守门 PR 上跑检查,不通过不能合并(第 16 课)第一道:编辑器/IDE 集成
配置 VS Code / JetBrains 的格式化与提示,保存即格式化。这让开发者几乎不用手动管格式。
第二道:pre-commit 钩子
用 pre-commit 这类工具,在 git commit 前自动跑检查和格式化:
# .pre-commit-config.yaml(Python 示例)
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.0
hooks:
- id: ruff # Linter
- id: ruff-format # 格式化
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.0
hooks:
- id: mypy # 类型检查效果:不合格的代码根本提交不上去——问题在离开你电脑之前就被拦住了。
第三道:CI 守门(下一阶段铺垫)
CI(第 16 课)在 PR 上再跑一遍全部检查,防止有人绕过本地钩子(git commit --no-verify)。
配置文件要进版本库(.pre-commit-config.yaml、ruff.toml、.eslintrc……),这样所有人共享同一套规则,新人克隆下来就有。
6、代码风格指南与"团队约定"
除了工具能管的,还有些团队层面的约定要写下来(放 CONTRIBUTING.md):
- 分支命名(第 13 课)
- 提交信息规范(第 12 课)
- 目录结构:
src/、tests/、docs/怎么放 - 错误处理策略:业务错误用异常还是返回值?日志在哪一层打?
- 依赖引入规则:加新依赖要经过谁同意?(防止依赖失控)
- 文档要求:公开函数要写 docstring 吗?README 维护到哪一级?
写约定的原则:
- 能自动化的就别写进文档(写在文档里的规则会被遗忘,写在工具里才生效)。
- 约定要短:一份读 5 分钟能读完、2 页以内的
CONTRIBUTING.md,比 20 页没人看的手册强。 - 定期复查:不合时宜的规则要及时删。
7、动手实验 🎯
任务:给你自己的项目装上三道防线。
- 选一个你熟悉的语言,配好格式化器(如 Python 用
ruff format,JS 用prettier),跑一遍项目,观察会改动哪些文件。 - 配好 Linter,修复它报的至少 3 个问题(区分哪些是真 bug、哪些只是风格)。
- 写一段故意含有 3 个典型陷阱的代码(如可变默认参数、裸 except、未使用变量),用 Linter 跑出来,记录每条告警。
- 配置 pre-commit 钩子,让"未格式化/有告警"的代码提交失败,然后验证它确实拦住了你。
- 写一份不超过 2 页的
CONTRIBUTING.md,包含:分支命名、提交规范、如何跑格式化和检查、目录结构、错误处理约定。 - 画一遍你项目的"问题左移"流程,标出你的三道防线分别在哪一步拦截问题。
做完这步,"风格之争"就彻底交给机器了——下一课进入开发方法论的核心:测试驱动开发(TDD)。
8、今天你带走的三句话
- 格式和命名交给工具,人只管结构和实践;统一的中等标准 > 不统一的最优标准。
- 静态检查的价值是把问题左移:IDE → 提交钩子 → CI 三道防线,越早拦截越便宜。
- 能自动化的规则就别写进文档,写进工具的规则才会真正生效。
下一课进入测试驱动开发(TDD)——一种"先写测试、再写代码"的开发节奏。