第15章 代码审查

下午的阳光从落地窗斜照进来,在会议桌上投下一道明晃晃的光带。光带里浮动着细小的尘埃,顺着光柱慢慢上升又落下。陈默推门进来的时候,小王已经坐在里面了——笔记本开着,屏幕的光照亮了他的脸。他穿着深蓝色的POLO衫,背挺得笔直,像是在等人检阅。桌面上摊着一本翻到中间的技术书,用一支签字笔压着页脚。

陈默坐下来,把笔记本接上投影仪。他打开GitHub PR页面,小王提交的代码出现在屏幕上。这是一次普通的周常审查——小王实现了一个支付接口的对接,调用第三方支付网关的API,处理回调通知,记录交易流水。代码量不大,五百行左右,涉及两个文件。看起来是一个标准的新人练手任务。

他扫码进了会议室,在桌边坐下,拧开矿泉水瓶盖喝了一口,开始走读代码。

第一眼他就觉得不对劲。他看到一个变量名叫a,另一个叫b,第三个叫temp1。三个String类型的参数,分别代表商户号、订单号和金额。他能猜出来——因为上下文线索太明显了——但他不应该靠猜。在工程代码里,一个变量名应该让读的人在一秒之内理解它的含义,而不是靠推理。

他继续往下滚动。一个函数里嵌套了三个三目运算符。那行代码看起来像一串被压缩到极致的逻辑表达式:return status === 'SUCCESS' ? (type === 'REFUND' ? refundAmount : payAmount) : (type === 'CANCEL' ? 0 : -1)。功能是正确的——根据支付状态和交易类型返回不同的金额。但这一行没有换行,没有括号辅助,没有注释,总长度超过一百个字符。他读了三遍才确认逻辑没有遗漏分支。

他没有马上说什么,继续往下看。后面的内容没好转。每个函数的长度都在一百五十行以上——有一个函数叫handleData,二百三十行,从参数校验到业务逻辑到拼装SQL到缓存查询到发送通知,五个不同职责的功能全部塞在一个函数体里,用几个换行符隔开就当划分段落了。注释一行都没有。

他继续往下走读,把代码从头到尾过了一遍。接口功能是跑通了的——单元测试写了,覆盖率大概在百分之七十左右,测试用例覆盖了正常流程和几个主要的异常分支。从功能交付的角度来说,代码是可用的。但这正是问题所在:它看起来能工作,但它不是可以被长期维护的代码。将来出问题的时候,接手的人会先花两个小时试图理解这些变量名和嵌套逻辑到底想表达什么,然后花一个小时把它们拆成可读的代码,然后才开始真正修bug。如果这个接手的人是凌晨三点被电话叫醒的,那这个过程会更痛苦——而这是完全可以避免的。

陈默沉默了一会儿,把椅子往后靠了靠,说:「小王,你来讲一下这段接口的整体思路。」这句话听起来是中性,但他掩饰得不彻底,语气里带着一丝没能完全压住的异样。

小王愣了一下,然后开始讲。他讲得很流畅——从接口定义说起,解释了第三方支付网关的调用流程,说到了签名算法和回调验证,再到异常处理和重试策略,最后还提了一嘴幂等性设计。思路清晰,条理分明。他对业务的理解很到位,这一点出乎陈默的意料——一个刚毕业的应届生能把支付业务的边界场景考虑到这个程度,说明他真的去看了文档,真的去理解了业务流程,而不只是把代码写完就交差。但这也让陈默更加困惑:一个对业务理解这么深的人,怎么能写出这样的代码?

当小王讲到第三个函数的时候,陈默打断了他。

「你为什么用三目运算嵌套?」

小王看了一眼自己的代码,似乎在回想那行具体的内容。然后说:「这样写更简洁,一行能写完。」

「一行写完不等于一行能读懂。」

「我能读懂啊。」小王说。他不是在抬杠,语气是诚实的——他确实能读懂自己的代码。

「三个月后的你读不懂。」陈默说,「你现在记得这里面的每一个分支是因为你刚写完。三个月后你接到一个线上的bug,凌晨三点打开这个文件,你不会想读三遍才确认逻辑对不对——你想一眼就看出来问题在哪。」

气氛僵了一下。小王没有再反驳,他性格里有一种不愿认输的倔强,但他的表情说明了一切——他的嘴角往下撇了一下,很快恢复了,但他的眼神显示他不完全认同。那是一种「我听到了但我还需要想想」的表情。陈默认得这种表情。他十年前在代码审查里被指出问题的时候,自己脸上也是这种表情。

他没有继续追这个问题,而是转向下一个文件。但心里那种不对劲的感觉越积越深。小王的理解能力没有问题,他对业务的理解甚至比大部分同龄人都好。但「工程」这件事和他之间存在一条鸿沟——他不知道什么是好的代码。不是因为他笨,是因为他从来没见过。学校里没有人教过他这些——数据结构课讲算法,编译原理课讲编译器,软件工程课讲瀑布模型和设计模式,但没有任何一门课告诉他:变量名要起得有语义、函数不能超过一百行、注释不是写给机器看的是写给下一个人看的。这些知识不是从课本上学来的,是在代码审查中被一遍一遍指出来之后才沉淀下来的。而小王显然还没有经过这个阶段。

陈默深吸了一口气,继续往下看代码。会议室里只剩下鼠标滚轮滚动的声音和空调低沉的嗡鸣。

审查进行到四十分钟的时候,赵恒推门进来了。他本来只是路过——透过会议室的玻璃隔断看到里面在过代码,就推门进来坐了一会儿。他拉了把椅子在角落坐下,翘起二郎腿,表情放松。他端着一个白色马克杯,里面是刚冲的速溶咖啡。他没有说话,但他的出现本身就让会议室的氛围发生了一种微妙的变化——陈默感到自己被观察了,被看着。好像赵恒想看看他怎么处理这个局面。

陈默继续往下过代码。他指出的问题越来越具体——参数列表太长,一个方法传了七个参数,调用方必须按顺序一个个填;函数没有单一职责,handleData那个两百行的函数应该拆成至少五六个独立的小函数;缺少必要的日志,支付接口的核心路径上没有记录任何关键日志,出问题的时候根本没法排查;异常处理里吞了错误,catch块里只写了一行return null,没有打印堆栈也没有记录上下文。

小王一开始还在笔记本上记——他写字的速度很快,在横线本上刷刷地记。但后来记的速度越来越慢,因为陈默指出问题的速度超过了他记录的速度。陈默能感觉到小王的抵触情绪在累积。他每指出一个问题,小王沉默的时间就多一秒。那种沉默不是「我错了」,是一种「我记了这么多条,但我不知道哪些是真的重要的」。

当陈默说到「这个函数名起得不好」的时候,小王终于开口了。不是反驳——是一个问题。

「陈哥,你说的这些我都记了。」他的语气很克制,但那种真诚的困惑从他的声音里透出来,谁都听得出来。他顿了顿,然后说:「但是——代码能跑不就行了?注释写了也没人看。命名规范公司也没有统一要求。单元测试覆盖率到了百分之七十。性能压测也过了。用户不会关心里面的变量叫什么名字,他们只关心支付成不成功。」

小王说这句话的时候语气是诚恳的。正因为诚恳,它更难反驳——他不是在抬杠,他是真的这么认为的。他的逻辑链是完整的:功能正确→性能达标→测试覆盖→交付完成。在这个闭环里,代码风格、可维护性、命名规范这些东西确实没有位置。因为他从来没有在一个需要长期维护的大型代码库上工作过。他没有被凌晨三点的电话叫起来修bug的经历,他没有见过自己三个月前写的代码然后想骂人的时刻。那些东西是经验带来的认知升级,不是靠逻辑推导能到达的彼岸。

陈默没有立即回答。他沉默了几秒。

让他看到了自己还没看到的东西。

他看着对面的小王。刚毕业,名校背景,理论基础扎实,算法和数据结构都能聊得头头是道。但这些知识是写在简历上的,不是写在代码里的。没有人教过他什么叫「工程代码」。代码不是写出来给机器执行的,是写给下一个维护者读的——而那个维护者,很多时候就是几个月后被线上问题叫醒的他自己。

陈默在心里把胸腔里的话组织了一下,然后开口说:「你的程序在功能上是对的,这没错。功能正确是最基础的要求,你已经做到了,这一点我先肯定你。」他顿了顿,「但工程不是一次性的。一个功能交付之后,它会被人维护、被人扩展、被人重构。我刚才挑你的那些问题,不是为了让你今天改完就算了——是为了让你半年后回头看自己的代码,不会想删掉重新写。」

小王没有说话。他低下了头,但不是那种「我错了」的低法——他是在思考。

赵恒在旁边笑了一声,说:「这话我当年也听别人说过。」

那一刻,会议室里的空气像是凝固了一秒。小王抬起头,第一次正眼看了赵恒——不是那种看领导的眼神,是那种「原来你也经历过」的眼神。陈默完全没想到,整个审查中最有价值的转折竟然是一个旁观者在角落里说了一句看似随意的话。陈默注意到了这个瞬间,他心里动了一下——这是今天审查中唯一一个真正的转折点。他的声音不大,但在安静的会议室里很清晰。

陈默看了赵恒一眼——他忽然意识到,赵恒在这个场景里的角色,就是当年那个老架构师。这个发现让他的思维突然停顿了一下——他没想到自己有一天会坐在这个位置上。他更没想到的是,这个位置带给他的一种他从未体验过的责任感。

他收回目光,重新聚焦在小王的代码上。他能感觉到自己的注意力比平时更加专注——因为他在说每一句话之前都需要先在脑子里过一遍。这比他自己写代码累多了,但也更让他投入。而他自己,已经坐到了老架构师的位置上。这个认知在他的脑子里像是一根琴弦被拨了一下,但没有时间细想。

赵恒看了看时间,站起来拍了拍小王的肩膀。那个拍肩膀的动作意味深长——不是鼓励,也不是批评,介于二者之间——好像在说「慢慢来」,又好像在说「好好听」。他的手在小王肩上按了不到两秒,然后走出了会议室。门在他身后合上了,发出一声很轻的咔嗒声。

会议室里的空调声重新变得明显。外面走廊里有人打电话,声音透过薄墙传进来,闷闷的,听不清内容。窗外有一群鸽子从对面楼顶飞过,在阳光中折了一个弯,消失在另一栋楼的后面。这些细节在安静的会议室里变得格外突出,像一首背景音乐突然被调高了音量。

陈默等了几秒,确认赵恒走远了。会议室外有脚步声远去,渐行渐弱,最终消失在走廊尽头。那个脚步声像一记节拍器,在他的胸腔里敲出了节奏——然后说:「我换个方式跟你说。」

他打开自己的IDE,新建了一个文件,把小王刚才写的那个支付接口的核心功能重新实现了一遍。他写得很快——但他每一步都会停下来解释。他先定义了一个DTO类,把散落在各个变量中的商户号、订单号、金额封装成结构化的字段,每个字段的命名都直接反映它的业务含义。然后他写了三个不超过三十行的小函数:一个做参数校验,一个处理支付逻辑,一个处理回调通知。每个函数的职责清晰到看一眼函数名就知道它做什么。他的手指在键盘上飞舞的时候,小王盯着屏幕,一个字都没有漏掉——那种专注的程度,像是第一次看到有人用正确的方式写代码。他在关键路径上加了两行日志——不是多余的,是在排查问题时必定会找的那两行。

他写了大概二十分钟。写完之后说:「你把我写的和你自己写的并排放在屏幕上,对比一下。」

小王照做了——他把自己写的和陈默写的并排在两个编辑器中,来回看了好几遍。他推了推眼镜,表情从困惑变成了某种介于理解和不甘心之间的东西。看了很久之后,他终于说了一句话:「我明白了。」

不是「我知道了」,是「我明白了」。陈默听出了这两个说法之间的区别。他在心里把这句话反复品味了两遍——这三个字里有一种困惑被解开之后才会出现的豁然感。当年他对那个老架构师说这句话的时候,那个人脸上露出的表情,他现在终于理解了。那种表情叫做欣慰。陈默低下头,手指在触摸板上无意识地滑动了两下。会议室的白板上还有上一场会留下的字迹,上面写着几个互联网行业的热门关键词,其中有一个他盯着看了好几秒——「判断力」。他不知道那是谁写的,也不知道那场会讨论了什么。但这个巧合让他心里动了一下。

「我今天不讲那么多条了,」陈默合上电脑说,「你先把这三条做好:第一,变量名和函数名起得让自己三月后还能看懂;第二,一个函数不要超过四十行,逻辑再复杂也要拆;第三,关键路径上要有日志。就这三条,你先在这一个PR上做到。下周我再看。」

小王说:「好。」

审查结束。陈默合上电脑说:「PR我先不合并,你先按刚才的思路重构一遍。有问题随时找我。」他站起来准备走,但犹豫了一下,又说了几句话。那些话不是提前准备好的,是在他站起来的那一瞬间突然涌到嘴边、他觉得应该说出口的话。

「代码质量这个东西,不是一天练成的。我写了十年,前五年写的也是你这样的代码。」这句话他说得很平淡,不是安慰,只是在陈述一个事实。但他希望小王能从这个事实里读出一点别的东西:没有人是一开始就会的,代际之间的传递方式就是一个人把自己知道的东西说给另一个人听。

小王沉默了一会儿,然后说:「陈哥,你刚才说的那个拆函数的思路,能再讲一遍吗?」他的语气和两个多小时前已经不一样了——不再是那个「我知道我该怎么写」的语气,而是一个「我想知道你是怎么想的」的语气。那是一个微妙的转变,但陈默注意到了。

「可以。」陈默又坐下来。

这次他没有用投影。他直接用自己的笔记本从头写了一遍示例——但一边写一边把思考过程讲出来。他写得很慢,每一步都会停下来确认小王跟上了没有。偶尔他会停下手里的键盘问一句:「这个地方你理解为什么这么拆吗?」小王有时候点头,有时候摇头。点头的时候陈默就继续往下写,摇头的时候他就换一个角度解释一遍。这种一对一的交流方式比刚才在大屏幕上过代码的效率高得多——没有压力,没有旁听者,没有一个人在角落里端着马克杯喝咖啡。只剩下两个程序员面对面坐着,在一段代码面前讨论应该怎么写。

陈默一边讲一边发现,有些自己做了十年已经变成下意识习惯的东西,其实很难用语言解释清楚。比如为什么他看到一个超过四十行的函数就会想拆——不是因为有规范强制要求,是因为经验告诉他不拆的话三个月后自己都会读不下去。这种判断不是知识,是手感。而手感这个东西,没有办法通过一次代码审查就传递给别人。

又写了大概十五分钟。陈默把剩下的部分留了个开头,然后说:「今天就到这里,你先消化一下今天说的东西。明天开始重构。重构的时候遇到拿不准的问题,先自己试,试完了还是不确定再来找我。」

小王把陈默写的代码从头到尾看了两遍。不是扫一眼——是真的在逐行阅读,就像在读一本教材。看完之后他说:「好。」他把笔记本合上,站起来。走到门口的时他停了一下,回过头说:「谢谢陈哥。」

「嗯。」陈默点了点头。

他也合上笔记本,走出了会议室。走廊里已经安静下来了。大部分同事工位上的灯还亮着,但键盘声稀稀拉拉的,偶尔有人站起来去接水,脚步声在水磨石地板上拖沓着经过。天花板上的日光灯有一根在闪,频率很慢,大概每隔两秒暗一次,再亮起来。他经过茶水间的时候,透过玻璃门看到赵恒在里面。赵恒背对着门,正在往马克杯里倒热水,腾腾的蒸汽在玻璃上蒙了一层雾。

陈默推门进去。

赵恒没有回头,只说了一句:「审完了?」他一边说一边把热水壶放回底座上,发出一声沉闷的塑料碰撞声。

「审完了。」陈默靠在台子边上,把马克杯放在台面上,没有倒水,只是需要一个放手的动作。

「怎么样。」赵恒转过身,端起杯子吹了一口气。

「能带出来,但需要时间。」

赵恒喝了一口咖啡,然后慢慢地说:「你知道我刚才为什么进去吗?」

陈默看着他,没有说话。

「我来公司第一年,也做过代码审查。那时候带我的那个人,就是现在公司的CTO。」赵恒靠在台子上,马克杯里的速溶咖啡冒着热气,在窗户透进来的光里形成一缕旋转的白线。他低头吹了吹,,语气很随意,像在讲一件很久以前的、已经不太重要的事。但他讲得很认真。「他在一次审查之后跟我说了一句话——'带人比写代码难十倍'。我当时不信。我觉得写代码才难,带人有什么难的?不就是告诉他怎么改吗?」他停了一下,又喝了一口咖啡,然后继续说:「后来我自己开始带人了,才发现他是对的。因为写代码你面对的是逻辑问题——输入是什么、输出是什么、中间怎么变换,每一件事都有明确的判断标准。但带人不一样。你面对的是一个活的人——他有他的理解方式,有他的知识盲区,有他今天状态好不好。你不能对一个人用if-else。你只能一遍一遍地尝试不同的输入,看他怎么输出,然后调整你的参数。」

陈默没有说话,但他在听。

赵恒继续说:「你今天做得不错。这年头能静下心来带新人的人不多了,大家都在内卷和加班,谁都顾不上谁。」他说这几个字的时候语气很平,陈默不确定这是真诚的评价还是上司对下属的一种例行鼓励。但赵恒又接了一句让他比较意外的话:「你没有直接说他写得烂。你让他自己先讲一遍,肯定了他对业务的理解,最后又用他面前写代码的方式做了示范。你在那给他写二十分钟的代码,比你在PR下面写一百条评论都管用。因为他是看着你是怎么写的——那个过程比任何说教都有效。」

陈默说:「我只是不想让他走弯路。」这句话说出口的时候,他意识到它听着有点装。但他说的是真心的。

「弯路是一定要走的。」赵恒看着他说。他的目光在说到这句话的时候变了一下——变得更专注了,像是一个人在说出他自己深信不疑的话时的那种表情。「你的作用不是帮他避开所有弯路——是在他走弯路的时候告诉他有人在看着,不会让他迷路。」

陈默把这个话在心里放了一会儿。办公室里的光线比刚才暗了一些,太阳正在沉到对面楼顶下面去。他把这句话放在脑子里转了转——表面意思很好理解,但下面还有一层他没有立即抓住的东西。

他低头看了一眼自己的双手。做了十年技术,写了十年代码,从一个小公司的初级开发一路做到现在的架构师。他熟悉各种框架的死角,知道线上问题排查的基本工序,能在一堆混乱的日志中快速定位到关键线索。但现在,他的工作重心正在从「写」变成「教」——从一个输出代码的人变成输出经验和方法论的人。他不知道自己适不适合这个角色,但他知道自己做了一个选择——选择留在这个位置上,而不是退回纯技术的舒适区。但他回想起小王最后说「我明白了」时眼睛里的那一点变化——那种从困惑到理解的微光,就像他当年在那个老架构师的代码审查中经历过的一样——又觉得这件事是值得做的。成长这件事不是一条直线,它在某些瞬间突然加速。而那些加速的瞬间,往往来自于另一个人在其关键时刻给出的、恰到好处的引导。

他把马克杯里的水喝完——他一直没有倒水,杯子里的水是他带来的那瓶矿泉水倒出来的,已经凉了。杯底在台面上磕出一声很轻的脆响。他把杯子放在水池边上。

「回去写代码了。」他说。

赵恒朝他举了一下咖啡杯,算是一个无声的回应。咖啡的热气在傍晚的光线里拉出一道斜斜的白线。

陈默走回工位的时候,窗外的光已经变成了那种傍晚特有的、带着一点橙色的柔和的光线。城市的轮廓在远处排列成一片参差不齐的剪影。几栋高楼的玻璃幕墙反射着落日的余晖,像是几块被点燃的矩形。中间那栋楼的楼顶有一个通信塔,顶端亮着一盏红色的警示灯,在暮色中缓缓闪烁。他的工位在靠窗那一排,夕阳正好打在他的显示器边框上,在屏幕上留出一道反光。他坐下来打开电脑,屏幕亮了,映出他的脸。

他打开工作群看了一眼——没有什么紧急消息,几条日常沟通,一个上线确认。手机在桌上亮了一下——微信消息提示,他瞥了一眼,是工程群里有人@了所有人。他没有点开,锁了屏继续看文档。他切到昨天还没看完的技术方案文档,开始逐行往下看。但扫了几行之后,他发现自己的注意力没完全回来——他拿起桌上的马克杯喝了一口——水已经凉了,不锈钢杯壁的冰冷的声音透过指尖传来,和嘴里残留的温热形成一种说不清的对比,不锈钢杯壁的温度透过指尖传上来,冰凉的感觉让他清醒了一些。他放下杯子,目光停留在屏幕上,但脑子里还盘旋着刚才茶水间里赵恒说的那句话:「你的作用不是帮他避开所有弯路,是在他走弯路的时候告诉他有人在看着。」他把这句话放在舌尖上品了一下。嗯。不是鸡汤,是一种他需要时间才能完全消化的东西。但在切页面之前,他停顿了一下。他想起刚才说自己前五年也写这样的代码时,小王脸上那种微微惊讶的表情。他不知道那是一种「原来你也经历过」的释然,还是另一种他还没想明白的决定——难道成长的标志就是这样?从被带的人,变成带人的人?,还是一种「原来你对我期望不高」的失望。但不管是什么,他会继续做这件事——因为总得有人做。就像当年那个老架构师为他做的一样。

很多年后,当他回想起自己职业经历中最重要的时刻,这一次代码审查会是其中之一。因为在这个普通的工作日下午——所有该发生的事情都在这个下午发生了,他第一次意识到自己已经从被带的人变成了带人的人。这个转变来得很安静,没有仪式,没有通知,没有晋升邮件。只是在一次普通的代码审查之后,在茶水间和一个老同事聊了几句话的时间里,突然在他心里坐实了——像一个悬了很久的决定终于落地。

窗外是一个极简风格的黄昏——没有云,没有霞光,只有天光由蓝变灰再变得透明,办公室里有种周末前夕特有的味道——键盘的塑料味和咖啡的焦苦味混合在一起。窗外的光线正在一分一分地暗下去。他打开了IDE,开始写明天的代码。这个场景让他想起以前在技术博客上看到的一句话:「在疫情和AI的双重冲击下,程序员唯一的护城河是判断力。」——不是ChatGPT写的,是某个前辈在知乎上写的。他当时觉得这句话矫情,现在回想却有点懂了。

他忽然明白了一件事。真正的判断力意味着什么——判断自己应该成为一个什么样的人。这意味着选择,也意味着放弃。

发表评论