如何阅读代码

简介:

身为一个程序员,工作中最重要的事情当然是写代码,其次就是读代码了。我们都是先阅读了别人的代码,才模仿着写下了自己的第一行代码。接手维护别人的项目,要读代码,遇到bug排查问题,要读代码,学习别人精妙的设计,同样需要读代码。从代码量上来说,绝大多数人所阅读的代码量远超自己写的代码量。所以程序员必须学会正确的阅读代码姿势,高效正确的阅读代码。

为什么读代码很难

读代码并不比写代码简单,阅读代码的困难源自以下几个方面。

首先,实现一个功能,存在多种具体的实现方式。即使是同一个思路的算法,最终产生的代码也有多种表现形式,不同代码的风格、变量的命名、if嵌套或者for/while的选择都会影响最终呈现的代码。阅读代码时,人脑充当了编译器的角色,不过通常意义上的编译,而是反向从代码的表现去理解代码的意图。眼睛看着代码,根据它在做什么反向推导它要做什么。更复杂的是,不仅要看静态的代码,还要在头脑中构造一个运行时的状态转换。

举个简单的例子,看到下面这段代码,你能分析出它在做什么吗

void do_somthing(){
    x := A[(hi + lo) / 2]
    i := lo - 1
    j := hi + 1
    loop forever
        do
            i := i + 1
        while A[i] < x
        do
            j := j - 1
        while A[j] > x
        if i ≥ j then
            return j
        swap A[i] with A[j]
}

上面的代码其实是快速排序的一次partition,很多人都熟悉,可能还比较容易猜得到它的目标。确定了一个目标,实现代码有好有坏,但是给出一个能正常工作的代码并不算很困难。例如实现一个排序功能,对大多数人来说可能都不是问题,但是从代码去反推行为反而更困难,写代码是顺着自己的思路,读代码是顺着作者的思路。

其次,一段代码的输入并不只是其参数,输出也不只是返回值。代码执行过程还会依赖各种外部状态:全局变量、进程外数据甚至网络上的数据。阅读代码时不仅要关注眼前的一段代码,还要考虑各种外部数据,考虑这些数据的结构以及能够对数据施加的各种操作,还有每种操作所导致的数据变化。代码运行过程中也会修改外部状态,阅读代码的过程中不仅要关注代码中自身数据的状态变化,还要考虑对外部数据的修改。

最后,实际的项目代码通常不是上面快速排序这么简单单纯,而是包含了复杂的概念和数据结构,众多的模块,各种各样的接口,冗长复杂的数据转换逻辑。每一段代码并非独立存在,而是作为一个更庞大整体的一部分。最要命的是代码里通常充斥着各种神奇补丁,以解决某个特定场景特定时间特定输入数据时才会遇到的问题,除非你能从其他渠道(例如注释或cvs log),否则想破脑袋也没法理解为什么这些代码的意图(别忘了读代码的目的就是分析意图)。

当然,有些代码由于作者能力问题,写出来的代码完全不具备可读性,这种情况不在讨论之列。

如何读代码

目的不同,阅读代码的方法也不同,为解决Bug而读代码和为掌握系统而读代码,所应使用的方式截然不同。如果为解决Bug而读代码,而且已经能快速定位到出现Bug的位置,那么直接分析相关的一小部分代码即可,运气好的话也能从代码中找到蛛丝马迹,顺利解决问题。如果接手维护现有的系统——无论是公司自己开发的还是直接使用开源软件部署——这时候就要完整的阅读所有的代码,以便掌握代码的方方面面,以后修改起来才能得心应手,出现问题也能快速定位和修复。有时候为了提升自己的能力,主动阅读一些优质开源软件的源码,学习其中的设计和实现,也要阅读完整的代码,或者某些模块的完整代码。后面这两种情况需要面对的代码量都很大,代码的实现通常也比较复杂,这时候就需要正确的方法。

不要急着读代码

读代码的第一要义,就是不要急着进入源码开始阅读。读代码不是为了把代码背下来,而是要掌握系统的结构,设计思路和关键模块的实现方法。整个项目通常规模较大,直接进入到代码里开始阅读,缺乏重点,没法区分该认真读的代码和该粗略读的代码,胡子眉毛一把抓,最终只能事倍功半。大脑里没有系统的整体结构,只看看一行行的代码,很难构建出系统的整体逻辑,只见树木不见森林。

想一下自己开发过的项目,相信很少有人能够记住某个文件某一行的代码。我们不会记住诸如某一行代码具体是什么这样的细节,记住的是代码实现的思路以及为什么要这么实现,哪里会调用这些代码,有什么样的输入,要返回什么样的结果。当然,还会记住项目的整体结构,运行环境,项目中的概念,各个模块的职责,每个功能的设计以及为什么要选择这样设计。

阅读代码不要尝试去记忆细节,从整体上把握。对项目中的设计,最好还要知道为什么选择这种设计方案而不是其他方案。能做到这些,对代码的掌握已经达到了代码Owner的水平。

从问题域出发

首先,你要明白要阅读的代码是做什么的。这个问题乍看上去很奇怪,都打算读代码了,还能不知道这些代码是干什么用?其实不然。很多项目包含的功能远多于我们所知道的,几乎所有的开源代码都包含着我们所不知道的功能,越是大型的流行的开源项目越是如此,因为开源项目的用户很多,部分用户针对自己的需求贡献了一些代码,这些需求如果不是实际场景中遇到,根本没法凭空想象出来。即使是一个公司的内部代码也可能包含很多奇怪的功能,尤其是年代久远的代码,充斥着各种已经废弃的逻辑。

为什么要先知道代码的功能呢?读代码的目的就是搞清楚代码做了什么,如果直接看代码,遇到自己没有考虑到功能,必然是一头雾水。如果已经知道了软件的功能,看到这些代码时就比较容易联想到它的意图了。

其实这里说的并不仅仅是代码功能,还包括代码的运行环境、开发语言、依赖的外部组件。一个运行在自建IDC内物理机上的系统,和针对云环境设计的系统,在一些技术方案的选择上有很大差异。一个运行在单机上的系统和一个集群模式运行的系统,技术方案的选择也会不同。语言、环境和外部依赖作为系统的约束条件,影响设计方案的选择和代码实现,了解这些信息,更容易推测代码的实现思路。

先看文档

文档是了解项目信息的最佳途径。一个好的项目至少包含用户文档和开发文档,用户文档站在用户视角描述项目安装部署和各个功能的使用,开发文档从代码实现的视角描述项目的架构、组件和关键设计。对于读代码,最关键的当然是设计文档,看完这个文档基本上就能对项目代码有个大致的了解。读设计文档时,重点关注这些内容:

  • 架构。系统包含哪些组件,各个组件的职责,组件之间如何通信。
  • 部署结构。系统运行环境,如何部署,需要什么样的配置。
  • 概念模型。不同的系统都有自己的概念模型,比如调度系统里的Scheduler/Worker/Resource,权限系统的User/Role/Group/Action,这些模型都会反映在代码里,理解这些模型才能理解代码。
  • 关键设计,每个系统里都会包含一些很关键的设计,有些设计是项目区别其他同类产品的核心,其他设计都是围绕着这个设计展开的。比如etcd的raft之于zookeeper的zab, rocksdb之于leveldb的compaction,docker的image和unionfs。

用户文档也可以大致浏览一下,不用细看,扫一眼目录,如果发现有些功能是自己不知道的,可以重点看看这些功能的介绍。

把握整体架构

读代码的时候并不需要去一行一行的阅读完所有的代码。掌握了整体结构之后,很容易判断出自己希望了解的细节位于哪个部分的代码里,接下来直接找对应的代码模块就可以了。掌握了整体架构,理解了每个模块的职责和输入输出,也能让后面理解代码变得更简单。

对于整体架构,需要掌握哪些信息呢?不妨尝试要求自己回答下面几个问题:

  1. 系统包含哪些组件
  2. 对于每个组件

    1. 职责是什么
    2. 运行在哪里,如何部署(是手工启动还是系统自动创建)
    3. 什么样的方式运行 ,单机、集群、主备
    4. 组件状态管理,组件本身是否有数据,数据存放在哪里
    5. 对外提供了哪些接口,这些接口谁会调用
    6. 对外接口的暴露方式,通信协议。对于集群/主备方式部署的组件,接入流量的转发模式,请求是通过什么方式分发到组件实例上的。
  3. 一个完整的操作,在系统里是怎么流转的。

概念模型、数据和流程

概念模型是软件对现实世界问题的抽象,一个软件项目中通常包含一组相关的概念模型。在设计系统时,我们有意识或无意识的将问题进行抽象,得到一组数据结构和数据结构上能进行的各种操作,然后用代码来实现。

从任何一个项目中,我们都可以找出其中的概念模型,不管项目是大是小。

概念模型并不是孤立存在的,项目中包含多个概念模型,它们之间也存在各种关系。一个系统中包含商品和订单两个概念模型,一个订单内又可以包含多个商品。

理清楚项目中的概念模型,模型间的关系,模型支持的操作,遇到相关的代码就能轻松理解代码的意图。

主次有别

在掌握架构和概念模型之后,对项目已经掌握了一大半。接下来可以开始读代码,但不是所有的代码都需要阅读。什么样的代码需要阅读?对于我个人而言,只有满足下面的条件时我才会去看其中的代码

  • 工作中遇到了问题,这个问题没有现成的解决方案,或者虽然能找到解决方案,但是希望知道更多的细节。
  • 知道项目中实现了某个功能,我自己有一些实现的思路,想知道它怎么实现的,和自己的方法进行对比。
  • 项目中有个神奇的功能,网络上也找不到实现方面的资料,很好奇它是怎么实现的,只能去看代码。

做事要分轻重缓急,读代码也一样,分清主次,找到需要读的代码,其他的代码等真正需要的时候再看也不迟。

自顶向下

找到要阅读的代码之后,同样不要忙着读,依然要用自顶向下的策略,先理清组件内模块和模块关系,构建一个具象化的逻辑视图。之后再按需阅读代码,这样的好处是要读的代码变少了,而且读的时候理解起来也更容易。那么什么是模块呢?这篇文章里,我们可以把模块理解成各个编程语言中组织代码的单位,比如Java/C++中Class。

很少有文档能够详细到描述一个组件内的实现,此时我们就陷入了一个困境:要想理清组件内的模块,我们不得不先读一次代码。

好在这个问题并不难解。分析架构的时候,我们已经分析了请求在组件间的处理过程,深入到模块内的代码后,我们还要在更低的层级上再分析一遍请求的处理流程。通过跟踪一次请求在代码中的流转,我们可以大致找出处理过程中涉及到的所有模块。有了这些信息,我们可以画一个简单的模块交互图,描述整个流程的处理过程。接着再从这个结构开始,逐个模块的分析模块的功能、接口和模块之间的交互。

对于面向对象编程语言的项目,通常包含抽象接口和具体实现,而且一个接口还有多种实现,有时候还会出现复杂的继承关系。理解这些代码时,一方面要弄清楚接口语义,另一方面通过具体的实现加深对接口的理解,在抽象和具体之间穿插前行。在一张图上画出所有的类、类的集继承和依赖关系,设计意图就容易理解了。针对小部分特别复杂的逻辑,画出流程图,更清晰直观。

如果代码的核心逻辑是处理数据,尤其是较为复杂的数据,此时要重点关注数据结构,数据在内存中的表示。搞清楚数据结构,再去分析操作数据结构的代码,顺序不能错,没搞清楚数据结构,不可能理解操作数据的代码。

区分独立的库

很多代码中都依赖一些独立的库,比如框架、中间件。遇到这些代码时,如果之前没有接触过对应的库,先停下来,找到对应库的文档,看看它的用法,千万不要直接跑到库的实现代码里去。了解这些库的用法,搞清楚正在阅读的代码通过库实现什么功能,做到这里就够了,代码原作者大概率也只是调用这些库,并不清楚库的内部实现。如果对库的内部实现感兴趣,想进一步了解,不妨换个时间再看。

工欲善其事,必先利其器。

一套好的工具能极大的提升读代码的效率。考虑到不同的语言工具并不相同,每个人也有自己的选择偏好,这里就不推荐了,只要确保你在读代码前已经配置好了自己的工具,在代码中能够轻松跳转,能够快速找到类型定义,能够查到哪些地方调用了当前函数。

从读代码到写代码

代码读的多了,自然就能理解什么样的代码是好代码,读起来赏心悦目,什么样的是垃圾,读起来让人抓狂。轮到自己写代码的时候,不妨结合读代码的经验,让写出来的代码可读性更好。想想自己的代码组织结构是不是清晰,概念是不是准确,变量命名是不是合理,有没有在合适的地方加上注释。最后,有没有提供对应的文档,文档有没有及时更新。

目录
相关文章
|
3月前
|
NoSQL Java 应用服务中间件
关于阅读源码
【1月更文挑战第12天】关于阅读源码
|
5月前
|
设计模式 JavaScript 前端开发
|
6月前
|
敏捷开发 算法 Cloud Native
面试中的代码写作:如何撰写清晰、高效的示例代码
面试中的代码写作:如何撰写清晰、高效的示例代码
64 0
|
7月前
|
消息中间件 设计模式 缓存
怎样更好地阅读源码?
最近,为了提高团队成员技术水平,考察了大家源码阅读情况。作为第一期任务,选择了spring框架,范围是spring-beans,spring-context,spring-core,以及spring-web。考核方式为:了解spring框架作用、核心概念,并选择感觉最重要的几个类进行详细阐述。
68 0
|
9月前
|
JavaScript 程序员 API
程序员为什么会在开发中阅读源码?
作为程序员的大家想必都会在开发的时候,去阅读源码。在实际开发中,开发者经常需要阅读和理解源代码,阅读源码是一种非常有用的技能,它可以帮助程序员更好地了解代码、解决问题、学习新技术和提高编码能力。阅读源码的过程实质上是对软件构建技术和架构深度的一种持续学习和理解。阅读源码可以揭示代码的内在逻辑,这被看作是对技术深度理解的一种体现,它能提高我们对技术的理解程度。结合阅读《Node 中的 AsyncLocalStorage 的前世今生和未来》这篇文章之后,我深刻体会到了作为开发者阅读源码的重要性和必要性。通过阅读这篇文章,我对 AsyncLocalStorage 的实现原理和使用方式有了更深入的理解
126 3
程序员为什么会在开发中阅读源码?
|
9月前
|
缓存 算法 安全
程序员写代码为什么要阅读源码?
阅读一篇技术文章,畅聊一个技术话题。本期文章推荐的是《Node 中的 AsyncLocalStorage 的前世今生和未来》,一起来聊聊开发者阅读源码的这件事。阅读源码的过程实质上是对软件构建技术和架构深度的一种持续学习和理解。阅读源码可以揭示代码的内在逻辑,可以对技术深度的理解,也能提高对技术的理解程度。然而,仅仅阅读源码并不能代替实践操作,因为通过实践,可以更加全面的理解代码的深度和进展。
101 1
|
11月前
|
缓存 程序员 API
【翻译】阅读优秀的代码
【翻译】阅读优秀的代码
56 0
|
设计模式 分布式计算 资源调度
如何阅读源码
如何阅读源码
166 0
|
机器学习/深度学习
Sorry About That, Chief!(阅读理解题)
题目描述 When Dr. Orooji was your age, one of the popular TV shows was “Get Smart!” The main character in this show (Maxwell Smart, a secret agent) had a few phrases; we used one such phrase for the title of this problem and we’ll use couple more in the output description!
107 0
|
编译器 C语言 数据安全/隐私保护
C++day12笔记无代码
C++day12笔记
309 0

相关实验场景

更多