读别人代码这件事,几乎所有开发者都躲不掉。接手历史项目、做 code review、在开源仓库里定位问题、给老系统加功能,这些场景的核心技能不是写,而是读。我见过不少人写代码很溜,一让他解释某个模块的来龙去脉就开始支支吾吾,问题往往不是他不懂,而是读代码的方法不对。这篇东西我想把最近读代码的一些思考整理出来,话题集中在“怎么把一坨陌生代码读成自己脑子里的结构”,涉及切入方式、信息筛选、辅助工具、常见坑,以及一些相对软性但同样重要的东西。
1. 读代码,先分清你在读什么
1.1 代码的四种类型,决定了你的读法
拿到一段代码,第一件事不是扑上去一行行看,而是先判断它属于哪种类型。不同类型的代码,正确的打开方式完全不同。
第一类是业务逻辑代码,比如电商系统的订单处理、CMS 的内容发布流程。这类代码的特点是分支多、状态多、异常处理多,通常和人家的商业模式强绑定。你读它的目的是搞清楚“某个业务规则是在哪里落地、怎么落地的”。
第二类是算法/内核代码,比如某个推荐系统的排序算法、某个数据库的存储引擎、某个数值计算的库。这类代码的特点是逻辑集中、性能敏感、数学或数据结构含量高。读它的目的是理解核心机制和复杂度。
第三类是框架/基建代码,比如公司内部的 RPC 框架、日志组件、配置中心 SDK。这类代码的特点是抽象度高、接口多、扩展点密集。读它的目的是学会怎么用、怎么扩展,以及出问题时怎么排查。
第四类是脚本/胶水代码,比如部署脚本、数据迁移脚本、CI 配置。这类代码的特点是“用一遍就完”,通常不讲究什么设计模式,但往往藏着大量环境相关的隐性知识。读它的目的是搞清楚“当时到底做了什么、会不会影响我现在要做的事”。
1.2 我常用的代码阅读切入点
判断完类型之后,我会按类型选择切入点。
业务逻辑代码,我习惯先找“状态流转图”。不管是订单、审批、任务还是工单,业务代码的核心通常是状态机。找到定义状态的枚举、触发状态流转的方法、状态变更前后的校验,基本就掌握了这个业务的骨架。
算法内核代码,我习惯先找“数据结构定义”。算法代码的入口可能很复杂,但内核通常围绕某一个核心数据结构展开。比如读一个跳表实现,先搞清楚 Node 结构长什么样、层数怎么分配,代码基本就懂了一半。
框架/基建代码,我习惯先找“SPI/扩展点”。框架代码的入口往往是一堆抽象类和接口,直接读实现类会迷路。先搞清楚哪些地方是给用户扩展的、哪些地方是框架内部调用的,再去看具体实现,思路会顺很多。
脚本/胶水代码,我习惯直接看“执行顺序”。脚本代码没什么深层设计,从上往下捋就完了。重点注意环境变量、路径、权限这些隐式条件。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从入口开始自顶向下,还是从数据流开始自底向上?
这个纠结几乎每个读代码的人都有过。两种方式各有严格的适应场景,选错方向会很痛苦。
2.1 自顶向下:适合业务系统,但容易迷路
自顶向下就是找到 main() 或者某个请求的 Controller 入口,跟着调用链一路往下走。这种方式在业务系统里很好用,因为业务代码的调用链是相对确定的:请求进来 -> Controller -> Service -> Mapper/Dao。跟着链走,很快能知道一个请求从进入到落库的完整路径。
但自顶向下有个致命问题:很容易迷路。业务代码的调用链上会有大量分支,每个分支都可能抛异常、回滚事务、调用外部服务。如果你在主线上走得太深,很容易忘记自己是从哪儿来的。我见过不少人读代码,两小时前从 Controller 入口出发,两小时后停在某个消息队列的消费方法里,完全忘了最初的目的是什么。
2.2 自底向上:适合算法内核,但容易只见树木
自底向上就是从最底层的函数开始读,一层层往上。这种方式适合算法内核代码,因为底层函数往往最稳定、最独立。比如读一个 B+ 树实现,从节点分裂函数开始读,理解了底层操作,再往上看插入删除逻辑,会轻松很多。
但自底向上的问题也明显:容易只见树木不见森林。底层函数读了一堆,但不知道它们之间怎么配合。我读一些开源算法库时经常有这种感觉——每个函数都看懂了,但串不起来。
2.3 我推荐的混合路径:入口 + 数据 + 关键点
我现在常用的方式是三段式的混合路径。
第一段,找入口。不管什么代码,入口总是存在的。业务代码是接口/监听器,算法代码是公开的调用方法,框架代码是生命周期回调。先花十分钟确定入口长什么样、接收什么参数、返回什么类型。
第二段,抓数据。这一步我最看重。搞清楚这个系统里“流动的数据”是什么——是一个订单对象、一条消息、一个矩阵,还是一个文件句柄。找到这个核心数据结构,然后跟踪它从头到尾经历了哪些转换。数据在哪里被创建、在哪里被校验、在哪里被修改、在哪里被持久化,串起来就是系统的实质。
第三段,攻关键点。入口和数据流都搞清楚了之后,整个系统的大框架就出来了。这时候再挑几个关键点深入研究——比如性能瓶颈、事务边界、并发控制点。这些关键点通常就是系统设计的精髓所在。
3. 画图是读代码最被低估的辅助手段
3.1 在纸上画调用链,为什么要用纸?
我读代码有个习惯:面前放一张白纸和一支笔。不是装模作样,是真的有用。
读代码的时候,大脑的工作记忆是有限的。一个复杂的调用链,可能有十几个方法在互相跳转,光靠脑子记,到后面肯定乱。把调用链画在纸上,等于把工作记忆“外挂”出来了。你不需要回忆“刚才那个方法是从哪儿调过来的”,低头看一眼纸就行。
用纸而不是用工具,是因为纸的随意性最高。你可以画箭头、画圈、画星号、写疑问,不需要遵循任何格式规范。读代码本身已经够累了,记录工具不应该再增加认知负担。
3.2 数据结构图比流程图更重要
很多人读代码喜欢画流程图,我倒觉得数据结构图更重要。原因很简单:数据结构的骨架作用远大于流程。
流程图告诉你的是“事情按什么顺序发生”,但数据结构图告诉你的是“系统的本质是什么”。比如读一个任务调度系统,状态机流程图当然有用,但如果你能画清楚任务对象有哪些字段、各种队列和集合里放的是什么、任务状态之间怎么流转,你瞬间就懂了整个系统。
我画的“数据结构图”一般包含三样东西:核心数据结构、数据之间的引用关系、数据在系统中的流动方向。画完之后,系统的边界、模块划分、依赖关系一目了然。
3.3 用 Mermaid 快速记录结构
虽然我推崇纸笔,但代码阅读过程中,有一部分内容是需要留存下来后续查阅的。这种情况下用 Mermaid 记结构化信息很合适。Mermaid 是文本化的图表语法,写起来快,还能直接渲染成图。
读代码时,我常用的 Mermaid 图有两种。一种是类图,用来记录对象之间的继承和组合关系。另一种是时序图,用来记录关键场景下的调用顺序。
比如读一个支付回调模块,用 Mermaid 记一段时序图:
code复制sequenceDiagram
participant App as 支付回调
participant Service as PayService
participant DB as 订单表
App->>Service: 回调通知
Service->>DB: 查询订单
alt 订单已支付
Service-->>App: 重复通知,直接返回
else 订单未支付
Service->>Service: 校验签名和金额
Service->>DB: 更新订单状态
Service-->>App: 处理成功
end
这种图记下来,几天之后再回来看这段代码,半分钟就能恢复上下文。比翻代码注释高效得多。
4. 命名与代码风格:第一层信息量
4.1 变量和函数命名是作者意图的压缩包
很多教人读代码的文章会告诉你“先看注释”,但我更倾向于“先看命名”。不是注释不重要,而是注释可能过期,命名则相对跟代码同步。
命名本质上是一个压缩包。一个变量叫 pendingRetryList,你立刻知道这是一个等待重试的列表;一个方法叫 validateAndResubmit(),你立刻知道它既做了校验又做了重新提交。好的命名能让你在不读实现的情况下猜对一半的意图,然后再去验证。
反过来说,如果代码里到处都是 data、temp、aaa、x1 这种命名,你就要有心理准备了——这段代码的作者没有在“可读性”上花心思,你可能需要花更多精力去推断他真正的意图。
4.2 代码风格传递出的工程文化
读别人代码时,我常会不自觉地通过风格判断作者的工程素养。一个模块里如果到处是缩进不一致、魔法数字散落各处、异常被吞掉只打一行 log,这个模块大概率维护成本极高。反过来,如果风格统一、常量和枚举管理得当、错误的处理路径清晰,这个模块通常质量可控。
风格不一致其实暴露的是团队协作的问题。比如一个文件里一会儿用两个空格缩进,一会儿用四个空格缩进,往往说明这个文件被多人改过,且没有人做 code review 的格式检查。这种代码仓库,读的时候要多留个心眼——你看到的逻辑可能不是某一个人深思熟虑的结果,而是多个人“堆”出来的。
5. 处理“命名混乱”和“超大函数”的实际经验
读代码总会遇到质量不怎么样的代码。命名随意、函数巨大、逻辑散落,这种代码读起来非常痛苦。这里分享几个我实际用下来的方法。
5.1 给自己建立“当前状态”意识
读烂代码最重要的一条经验:给每一步推理建立一个清晰的“当前状态”。无论这个函数写得有多乱,你每看完一小段,都要在心里回答一个问题:“数据跑到这里,已知什么、未知什么、可能的状态有哪些?”
比如读一个有 500 行的函数,你不需要一次记住 500 行。你可以把它切成多段,每段的末尾确立一个中间状态。这个中间状态就好比一个检查点,只要检查点没断,后面就算再绕,你也能靠检查点找回来。
5.2 如何快速定位“到底谁是入口”
烂代码里经常有这种情况:一堆类互相引用,你根本不知道从哪个类开始读。这时有个小技巧:看数据的流向。
先从调用关系上找一个“源头类”,然后看它的数据从哪里来。如果数据从构造函数来,就找谁 new 了它;如果数据从方法参数来,就找谁调用了这个方法。顺着数据来源往上追,不用几跳你就能找到入口。
5.3 测试用例是最好的文档
这个经验我在多个项目中反复验证过。很多代码注释烂、命名烂、设计烂,但测试用例写得还行——或者说,测试用例至少透露了作者期望的行为。
读这种代码时,我会先找 test 目录,看测试方法名。测试方法名通常会描述“它应该干什么”。比如 test_should_increase_credit_when_payment_success,虽然生产代码里变量叫 a、b、c,但从这个测试名你就能推断出这段代码的业务语义。
5.4 用 git 历史辅助理解
如果一段代码实在看不懂,试试 git log 和 git blame。代码里的“为什么”往往不在代码里,而在 commit message 里。我看过一个处理库存扣减的模块,代码逻辑很怪——先扣减再校验。单看代码你会觉得这写反了,但翻了 git 历史才发现,原来是某个版本为了应对超卖问题加的一个妥协方案,commit message 里写清楚了原因和 trade-off。
这个经验特别有用。读代码遇到“看似不合理”的逻辑,先别急着喷作者,先看它是不是有历史遗留原因。
6. 换位思考:从作者角度看代码
6.1 理解历史包袱
代码是人写的,人有当时的局限。你看到一个“设计很糟糕”的模块,先别急着下结论,想一想它在当时的历史条件下是怎么一步步变成这样的。也许它诞生之初很简单,但随着需求迭代,不断打补丁,慢慢就臃肿了。这种理解不是给糟糕代码洗白,而是帮你更准确地判断“这段代码是不是真的该重构”。
认识历史包袱还有个实际收益:你知道哪些地方是“雷区”。比如看到某个类的实现非常绕,往往是积累了很多边际 case。你看懂了它为什么绕,反而不会乱改它。
6.2 需求演进对代码的影响
代码是需求的产物,而需求一直在变。有些现象,比如代码分支特别多、if else 嵌套很深,通常都是需求快速演进的结果。作者当时的处境往往是“先上线再说”,没时间做优雅的抽象。懂了这个背景,你对“不完美”就会有更多容忍,也能更准确地判断哪些代码是核心的、哪些是边缘的。
6.3 读代码的最高境界:以“合作者”身份进入
写到这里,其实想说一个读代码的心态问题。我发现技术人很容易在读别人代码时带上有色眼镜,看到不顺眼的地方就很想重写。但一个好的读者,应该以“合作者”而非“批判者”的身份进入代码。
什么是合作者心态?就是假设作者是一个聪明人,他写下的每一行代码在当时看来都有其合理性。你的任务不是去证明他写得不好,而是去搞清楚他为什么这么写。带着这种心态读代码,你会更容易理解系统的设计意图,也更容易在之后的修改中保持风格的一致性。
改别人的代码,尤其是历史项目的代码,最忌讳的是“不尊重现状”。你可以在提出重构建议时很激进,但在真正动手改之前,必须先成为一个“理解现状”的合作者。否则你改出来的东西,大概率会破坏原有的一些隐式约定。
老实说,读代码能力的训练,没有太多捷径,就是大量地读、系统地读、带着方法地读。但每一次有意识地选择切入点、画一张结构图、追问一句“为什么”,都会让你在下一次读代码的时候更快、更准。希望这篇东西能对你的读代码思路有一点触发。
