ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

代码审查实战指南:从形式主义到体系化质量保障

代码审查实战指南:从形式主义到体系化质量保障 在团队里做了近十年的代码审查我越来越确认一个判断代码审查Code Review能做到什么程度根本不取决于流程表单画得多漂亮而取决于团队里每个人对审查到底是干什么这件事有没有共识。很多团队不是没有Review机制而是把Review用成了点通过的按钮游戏很多程序员也不是不想把代码写好而是没人告诉他好的边界到底在哪里。这两件事叠加在一起代码质量自然就成了玄学。这篇指南想解决的就是这个问题。我会从形式讲到实效从单次审查讲到体系搭建把代码质量从靠个人自觉变成靠机制兜底。内容主要面向三类人刚刚开始参与Review的新人、正在推动团队审查流程的负责人以及厌倦了审了等于没审的资深程序员。如果你曾经在评论区写不出有价值的建议或者对着一份5000行的PR无从下手这篇文章应该能给你一些可以直接用的思路。1. 代码审查为什么容易沦为形式主义先看清这场游戏的本质1.1 把审查当成验收还是协作——定位决定后面所有动作我观察过很多团队的Review现场发现大家最常犯的错误是把代码审查当成验收环节。审查者像海关查验一样只看你的代码最后是不是能跑、风格是不是合规、有没有明显的大毛病然后给一个通过或不通过的结论。这个心态的潜台词是写代码是作者的事出问题也是作者的事我只是把最后一道闸。这种定位下Review基本不可能有实效。因为验收心态天然是防守性的而不是建设性的。审查者不会主动去理解业务的来龙去脉不会去思考这段代码三个月后会被谁修改更不会愿意花时间讨论有没有更好的设计路径。大家都忙既然我已经审阅过了那责任就撇清了剩下的交给测试和线上故障就行。另一种更常见的变形是走过场心态。比如团队规定合并必须至少有一个approve于是大家心照不宣地互相点LGTMLooks Good To Me再比如周五晚上赶上线拉了个快速评审会议30分钟看完800行改动所有人其实都没来得及读。这种流程跑下来唯一的产出是提交记录里多了几个Approved代码质量该是什么样还是什么样。我个人的理解是代码审查的本质是知识传递风险前置。写代码的人拥有完整的上下文——他知道为什么要这么改、考虑过哪些方案、绕过哪些坑审查者拥有外部视角——他不知道上下文所以能发现逻辑漏洞、命名误导、边界遗漏。两边通过对话把双方的认知差抹平最终一起对这段代码负责。这个定位听起来有点理想化但只有先想清楚我们在干什么后面所有的方法论才立得住。1.2 三个让Review变形的常见操作形式主义不是天生的是具体操作喂出来的。我总结了三个最常见的变形成因你可以对照自己的团队看看是不是也在踩第一个一次性提交上千行。这是Review的头号杀手。800行以上的改动任何正常人都很难从头到尾读清楚。审查者能做的只剩下扫一眼点个通过或者干脆滑到评论区随便说两句。这个问题的解法很简单——拆PRPull Request我后面单独展开。第二个没有统一的知识基线全凭个人口味。有的审查者特别关注命名有的只关心性能有的整天揪着代码风格。同一份代码换个人审结果是完全不一样的。没有基线Review就变成了随机抽查而不是质量保障。这不是某个审查者的错是检查项没有被明文化。第三个反馈链路断裂。审查意见提出来了结果作者改了一行就推到仓库压根没回复各条评论又或者审查者提了建议作者改完之后也没有重新去讨论。评论记录留在那里看起来沟通过了实际上质量问题是原地踏步。评论一次回执一次改完再答复一次——这个闭环不能断。我印象很深的一次事故某次上线前临时加需求大家在一个大PR上快速approve结果漏掉了一个并发场景下的缓存同步问题上线后直接导致订单状态错乱团队通宵回滚。那天晚上我就意识到形式化的Review比不Review更危险——因为它给了所有人已经检查过的虚假安全感。2. 从哪几个维度展开审查一份能直接抄作业的检视清单很多人觉得不知道怎么审其实就是缺少一个明确的清单。这里我整理了一份自己在日常Review里用的检视维度按优先级排序。你不需要每一条都在每个PR里严格过一遍但至少要让团队有一份必须检查的基线清单。2.1 正确性与边界条件不要只盯着happy path我见过太多Review讨论的重点全在主链路上——这个函数调对了这个接口返回正常——然后在高并发、空数据、异常输入上栽跟头。代码的正确性恰恰体现在边界条件和异常处理里。具体来说建议审查时重点追问这几个问题空值风险从列表、字典或数据库结果中取值时集合为空怎么办first()有没有可能抛异常nullable的字段有没有可能在逻辑中被当成非空使用并发场景这段代码涉及共享状态吗缓存和数据库之间的一致性怎么保证分布式环境下的锁粒度合适吗异常处理catch之后是吞了异常还是记录了日志异常路径上的资源连接、流、锁、临时文件有没有释放重试逻辑会不会因为网卡问题导致超时堆积数值与时间金额计算用的是浮点还是定点数时区转换有没有考虑夏令时时间窗口边界上的数据会不会重复或遗漏这些问题不需要每次全部检查但至少应该成为审查者的肌肉记忆——不是只在看到明显问题时才想起来而是在读每一段关键逻辑时自动过一遍。2.2 可读性、命名与注释代码是写给机器跑的更是写给人看的如果说正确性问题影响这次上不上线那可读性问题影响的就是下次改这坨代码的人会不会哭。我在Review里最常给出的三条意见说出来都很基础但真实项目里就是反复出现命名是否传达了意图。变量名叫data、temp、result函数名叫process、handle、deal这些名字没有信息量。好的命名应该让人不读实现就能猜出大概。比如applyCouponToCart就比processCart清晰得多。函数是否单一职责。如果一个函数同时做了解析参数、查询数据库、组装返回结构、发消息通知四件事它就该被拆开。拆开的好处不仅是可读性更是可测试性——你可以单独验证每个环节。注释是否解释了为什么而不是是什么。给价格加10%这种注释毫无价值真正有价值的是因为XX规则要求下单超过100元的订单需要额外加收10%服务费。前者你删了也不影响理解后者能救继承者一命。我自己的经验是命名和注释这类问题最好在PR阶段解决不要拖到维护阶段。因为维护阶段没有人会再去翻历史记录大家只会对着眼前这团看不懂的代码默默骂人。2.3 健壮性、安全性与性能容易被忽略的隐性成本这部分通常是资深审查者和新手审查者的分水岭。新手只会看代码能不能跑经验丰富的人会看代码在恶劣环境下能不能扛住。建议关注的点有输入校验对外接口是否做了参数校验数据来源是否可信恶意输入超长字符串、非法编码、错误类型会不会导致系统异常权限与越权这个接口的鉴权逻辑放在客户端还是服务端横向越权A用户查看B用户数据有没有考虑关键操作的审计日志有没有埋敏感信息日志里有没有打印密码、Token、身份证号、手机号错误信息抛给用户时会不会泄露内部实现细节性能隐患循环体里有没有发HTTP请求、查库、打日志有没有N1查询大列表的内存占用有没有评估热点路径上的同步锁会不会成为瓶颈这些问题如果等到线上出故障再排查成本是PR阶段的十倍以上。在Review里发现性能隐患是性价比最高的修bug方式。2.4 可测试性能不能改就看好不好测这个维度我放在最后但它在长期维护里非常重要。一个PR如果合入之后几乎没法写单元测试那它就是在制造不敢改的代码。审查时可以用一个简单的判断方法如果让我为这段逻辑写单测我能不能不依赖数据库、不依赖真实网络、不靠反射爆破私有状态如果答案是不能那这段代码的设计大概率有问题——它缺少依赖注入、接口抽象或者把太多具体依赖耦合在了一个函数里。可测试性和代码质量是强相关的。能写测试的代码通常边界清晰、职责单一、依赖可控不能写测试的代码往往是一坨揉在一起的意大利面。所以审查时遇到不好测的代码不要急着说加个测试吧可以先问:这段逻辑能不能拆一下、注入一下依赖让测试变得容易写3. 审查节奏与工作流设计让Review嵌入日常而不是打断日常清单解决的是审什么流程解决的是怎么让审查真的发生、真的有效。很多时候不是大家不愿意审而是流程设计得让人没法认真审。3.1 小步提交把PR控制在可审查的范围这是我认为性价比最高的一条流程改进——把PR拆小。为什么大PR是Review的敌人因为人类大脑的工作记忆是有限的。读200行代码和读800行代码对注意力的消耗不是4倍而是接近指数增长。审查者面对大PR本能反应就是跳跃式浏览重点全丢了。我在团队里的实践是这样拆PR的按逻辑顺序拆比如新增一个积分功能拆成第一步数据库表结构与迁移→第二步积分计算服务→第三步接口层→第四步前端接入。每个PR只做一件事后一个PR基于前一个PR的分支。基础组件先行如果有一个工具函数、一个基础类被多个模块依赖先单独提一个PR合入再基于它开发上层逻辑。这样上层PR的diff会小很多审查时上下文也清晰。控制量级单个PR尽量控制在200-400行修改以内。如果超过400行先停下来想想是不是有拆分的空间。约定俗成大家都会自觉遵守。小PR带来的副产品也很明显回滚风险低、冲突概率低、对并行开发友好。团队review速度上去了大家反而更愿意认真看。3.2 时效性、异步与同步的选择代码审查还有个隐性成本——上下文丢失。作者写完代码时对每一行都记得清清楚楚但只要过了一周再让他解释当时的决策可能自己都要翻半天历史记录。所以Review的响应时效非常重要。我的建议是正常PR在24小时内给出第一轮review意见紧急PR在2小时内响应。这里说的响应不一定是完整撸完所有代码可以是一句我已经看到了正在看明天中午给意见让作者知道有人在管这个事不会心里没底。另外异步和同步怎么选我的经验是常规功能PR走异步review大家在自己的节奏里读代码、写评论思考质量更高。紧急修复拉语音/当面过一遍是最高效的。因为这时候时间最贵面对面沟通能瞬间补齐上下文避免异步来回好几个回合才弄明白对方在说啥。架构级PR不应该在pr阶段才review。架构和设计方案应该在文档阶段或被会议评审PR阶段只是验证实现方案是否落地。如果团队里常出现PR里吵架构的场面说明设计前置没有做好。流程层面还有一个容易被忽视的点PR描述一定要写清楚为什么改。很多人PR描述只写修复bug优化性能上下文全靠审查者从几千行diff里反向推理。我建议PR描述里强制包含三块背景为什么改、方案怎么改、验证本地/测试怎么证明是对的。这玩意儿不光是给别人看的三个月后你自己回来看这个PR也会感谢当时的自己写了描述。4. 交互中的艺术怎么提意见别人才愿意听如果说前两章是在谈事这一章要谈人。代码审查表面上审的是代码实际上一半以上的阻力都来自人际互动。同一个意见表达方式不同效果天差地别。4.1 把评论分级Must fix、Should、Nit我最开始审代码的时候每条评论的权重都一样结果就是作者分不清哪些是必须改的、哪些只是锦上添花。后来我引入了一套简单的分级体系沟通效率一下子提高了Must fix必须改明确的功能错误、安全问题、明显会引发线上故障的逻辑。这类不用商量改了就完。Should建议改代码可读性差、设计不够优雅、潜在边界问题。这类有商量空间但通常还是建议处理。Nit吹毛求疵命名偏好、风格细节、注释拼写。这类可改可不改作者有最终决定权。分级的价值在于它给作者明确了哪些事一定要做哪些事是可选优化减少了无谓的拉扯。同时审查者也可以凭此管理自己的沟通火力——Nit问题不要刷屏一次PR里提两三条就够了太多会淹没真正重要的Must fix。4.2 用提问代替断言这是我从一位老前辈那里学到的后来成为了我Review的默认风格。你这里错了应该用XX和这里有没有考虑过XX情况给作者的心理感受是完全不同的。后者更像是在共同探讨前者则像是在下判断。举个例子。看到一段代码def get_user_info(user_id): query db.select(SELECT * FROM user WHERE id ?, user_id) return query.first()断言式评论是这里要用判断不然用户不存在时会报错提问式评论是first()返回None的话调用方有处理吗会不会在登录流程里解引用两种表达传递的结论是一样的但提问式给作者敞开了讨论空间有可能调用方确实做了空判断只是这段代码里看不出来也有可能作者真的漏了你的提问正好点醒了他。把你错了换成咱们一起确认一下对抗情绪会大幅下降。4.3 接受合理的驳回审查不是命令链。作者对上下文的理解通常比审查者更深所以他有权利对你的评论说不。只要作者给出了合理的理由——比如性能约束、业务妥协、历史遗留决策、兼容性要求——审查者应该选择接受。我自己踩过的坑曾经坚持让一个同事把循环里缓存查询结果改成一次性批量查询理由是减少数据库IO。结果他给我看了一份压测数据这个接口的QPS极低数据库根本不是瓶颈反而批量查询把SQL改复杂了可读性变差了。那一次我学到的道理是——Review意见要基于场景不要基于教条。你说批量查询更好没问题但要在理解了业务场景之后再说。4.4 把个人偏好拦在门外每个程序员都有自己写代码的口味有人喜欢打空行有人不喜欢有人爱用lambda有人认为一律用普通函数。凡是审美层面的东西交给格式化工具和团队规约去统一不要让它在Review里消耗任何人的情绪。真正值得Review讨论的是正确性、健壮性、可读性这些实质问题。一个实用的建议把团队的编码规范、格式化配置比如Prettier、黑格式化工具提前放在项目配置里让CI强制执行。这样在PR里再看到这行缩进不对这里加个空行吧这类评论直接回复去跑一下格式化工具就行——既省口水又避免冲突。5. 从单次审查到体系化代码质量沉淀、度量与传承最后这一段写给想要更进一步的人。单次Review做得再好也只是点状提升真正让代码质量稳定向好的是把审查经验沉淀成体系。5.1 把确定性规则交给自动化让人力专注在判断上代码审查最大的浪费是人去检查机器能干的活。lint、格式检查、静态安全扫描、基础单元测试覆盖率检查这些全部应该交给CI/CD去卡。人的时间应该用来判断那些机器判断不了的问题设计合理性、边界逻辑、业务语义、可维护性。我在团队里定的原则是凡是能在CI里写出来的规则一律不在Review里讨论。排名靠前的评论内容命名建议、代码风格、安全扫描结果会被逐步固化成自动检查的一部分。这样reviewer的注意力就能集中到这段代码的设计有没有问题上。5.2 用数据度量审查质量但不纠结单一指标代码审查要不要量化我一直觉得要但要小心别让指标异化。比较有参考意义的指标有这几个指标含义使用注意Review覆盖率合入主干的PR有多少经过至少一次review理想目标是100%但别只看数字还要看评论深度平均首轮响应时长PR创建到第一条review意见的时间反映流程响应效率过长意味着上下文丢失风险每PR评论数分布每条PR收到的评论数量长期为0说明流程虚设暴涨说明前期设计质量不高问题类型分布Must fix/Should/Nit的比例按维度归类正确性/可读性/安全等用来发现团队共性短板最后一项尤其有价值如果连续几个迭代周期Review里发现的都是同一类问题说明团队在这块存在系统性短板。比如安全性问题反复出现那就在设计前置阶段加一个安全清单边界条件漏处理反复出现那就在开发自测阶段补一个模板。度量的目标是发现系统性问题而不是给个人打分。5.3 经验库与新人培养让每个PR都变成一堂微课代码审查有一个隐藏的副产品——它是团队内部最好的知识传递渠道。一个新人通过Review阅读经验丰富同事的diff比听三场技术分享收获都大。反过来老手在Review新人代码时也能把团队沉淀的开发策略一点点传下去。所以我会建议团队维护两份资料常见问题库把历次Review中反复出现的高频问题整理成一个按维度分类的checklist文档新人入职时就发给他。这等于把团队的踩坑史提前交给了新人让他们不要在同一个坑里再摔一遍。评审要点模板把PR描述模板、评论分级约定、审查要点都沉淀成一份团队Wiki。新人刚参与Review时照着模板走至少不会完全不知道看什么。我常说一句话代码审查的最高境界不是每次都能抓到bug而是抓bug这件事不再依赖某几个人的火眼金睛。当团队有了共同的评价标准、好用的工具链、清晰的分级沟通方式高质量代码就不再是大家靠自觉挤出来的奢侈品而是流程运转后自然涌现的结果。说到底代码质量不是一个终点而是一条持续迭代的路。代码审查作为这条路的重要枢纽可以从一次认真的Review开始也可以从今天这篇清单的某一条开始。比起追求形式上的完备我更希望你至少做到一件事下次Review时别只是点个通过。认真看完那段diff把一个真实的问题摆在评论里你会发现代码变好的速度比想象中快得多。
返回列表