前言
上周我在和一位年轻程序员聊天时,他问到我:“如何阅读源码?”,我们讨论了一段时间,我还列举了几种有效阅读源码的方式。然后他说:“你应该就这个话题写篇文章,这对初学者很有帮助,而且这种经验无法从书籍和教程中获得。” 那么开始吧,下面是我关于阅读源码的小技巧。
为什么我们需要读源码
我们程序员每天都要和源码打交道。经过数年的学习,大多数程序员可以“写”代码,或者至少是拷贝并修改代码。而且,我们教授编程的方式强调编写代码的艺术,而不是如何阅读代码。当我说“阅读代码”,我是指有意地专门阅读代码。
众所周知,编程和写作有诸多相同之处。唐纳德·克努特甚至引入了文学编程(literate programming) 编程范式。编程与写作有相同的理念:表达我们的想法 。还记得你在学校是怎么学习写作的吗?我们的写作能力来源于从小学开始直到现在的大量的文本阅读。多年以来,我们阅读了不同难度的伟大作家的作品,并练习了多种写作技巧。
“如果你没时间读,你就没时间(或工具)写,就这么简单。” —— 斯蒂芬·金,《写作这回事:创作生涯回忆录》
正如斯蒂芬·金所观察到的那样,一个作家必须广泛而频繁地阅读,才能形成自己的声音, 并学会写出促使读者拿起书并痴读的句式和故事结构。 和读书一样,有意地阅读代码可以帮助程序员加速成长,尤其是对中级(intermediate)程序员而言。 这样做有三个好处。
站在巨人的肩膀上
我们从他人身上学习。优秀的源代码就像文学杰作,它不仅仅只提供了知识和信息,还提供了启迪。
通过浏览Linux内核、Redis、Nginx、Rails或其他著名项目, 你可以从全球范围的成千上万的顶级程序员那里汲取智慧。在这些项目中可以找到无数的良好编程示例、编程范式选择、设计和架构。向他人学习的另一个好处是能够避免常见的坑,大多数坑早已被他人踩过。
解决困难问题
在你的职业生涯中,你终将会碰到谷歌都无法解决的问题。如果你还没碰到过这种问题,这只是因为你编程的时间还不足够长 :)。阅读源码是调查这类问题的好方法,也是学习新东西的好机会。
扩展你的边界
大多数程序员只在少数特别领域编过程。一般而言,如果你不时常推自己一把,你的编程技能会维持在你同事间的平均水平。不要满足于修补bug或在现有系统中添加琐碎特性的工作。相反,你可以试着扩展到一个新的领域,持续尝试找到一个你在日常工作中接触不到、但你感兴趣的领域。这将从整体上拓宽你对编程的理解。
应该读什么样的源码
综上,阅读源码是有益的。那么下一个问题,有这么多优秀作品可供选择,我们该选择并阅读什么样的源码呢?你必须从选择目标开始。如果不在这个步骤上下点功夫,你从源码中学习的效果就会打折扣。这里有一些典型场景:
记住,选择与你当前的编程技能与知识水平相当的项目。 如果你选择了远超你当前技能水平的项目,最终你会感到沮丧。读一些相对较小的项目,接着读更大的项目。如果目前你不能理解某些特定的代码片段,这意味着你有个知识缺口(knowledge gap)。把代码放到一边去,试着读一些相关的书、论文或其他文档,当你更有信心时再回来接着读代码。我们总能在一个模式中取得进展:读(代码、书、论文),写,更多的读,更多的写。
如何读源码
《How to read a book》 是一本指导人进行明智地阅读的书。作为初学者,我们值得投入时间和精力去思考我们应该如何阅读代码。 阅读代码不是件容易的事。 光是阅读源码是不够的,你要试着去理解他人的设计和想法。
预先准备
为了更有效率地阅读代码,你需要提前在手边准备这些东西:
阅读过程不是线性的。你不会就那么一个接一个地读源文件。相反,大多数时候我们会从顶到底地阅读代码。下面是一些更有效率阅读代码的小技巧:
结合上下文阅读代码
当你阅读代码时,请持续提出问题。例如,如果一个应用有缓存策略,一个好问题就是:如果键无效了会怎样?缓存中的值如何更新?带着这些问题阅读代码,就是结合上下文。或者说因为你有了一个目标,你会变得享受阅读的过程。你甚至可以自己做一些假设,然后在代码中寻找验证。
你有点像侦探:你想发现代码的真相,代码的逻辑,代码是如何像故事一般上下流动的。
把实例跑起来并与之交互
源码就像乐高积木,只是已经组装好了。如果你想了解它们是怎么组装在一起的,你需要和它交互,有时甚至要把它拆开。阅读同一模块的老版本同样有帮助。从Git中阅读版本差异,试着弄清楚特定的特性是如何实现的 (修改日志在这个场景很有用)。举个例子,我发现Lua的第一个版本相当简单,这可以帮助我了解作者最初的设计理念。
Debug是另一种与代码交互的方式。试着在代码中加一些断点(或打印一些变量值), 然后弄明白打印到控制台中的所有输出。
如果你对代码了解比较透彻了,试着对代码做一些修改,重新build并把它跑起来。最简单的方式是试着调整配置项,去看不同配置的运行结果。之后你可以试着添加一些细微的特性。如果这些特性对其他人也有用,你应该把代码贡献到上游。
了解数据结构间的关系
“糟糕的程序员担心代码,优秀的程序员担心数据结构和它们的关系。” -Linus Torvalds
数据结构是一个程序中最重要的元素。用笔或者你喜欢的其他工具画出数据结构间的关系。这个图就是源码的映射。你会在阅读过程中时常参考这个图。一些工具比如scitools 可以用来生成UML类图。(译:这个方法用在写代码中能节约翻Model声明文件的时间,推荐用纸笔,不占屏幕)
了解模块间的依赖关系与边界
大项目中会包含许多模块,一个模块经常只拥有单一职责。这有助于我们减少代码复杂度,在适当的层级上做抽象。模块的接口是抽象的边界,我们可以一个接一个地阅读模块。如果你在阅读一个使用Make构建的C/C++项目,Makefile是了解模块间如何组织的好切入点。
边界本身也很有用。优秀的代码组织得很好,变量名与函数名的命名风格体现着可读性。你不需要阅读全部源文件,你可以忽略不重要的或你熟悉的部分。如果你确定一个模块是仅仅是为了被解析而设计的(just designed for parsing), 那么你已经大致了解了它的功能;那么你就可以跳过不读这个模块。当然,这将大大节约时间。
使用测试用例
测试用例也是帮助代码理解的一个很好的补充。测试用例就是文档。 当你在阅读一个类时,试着把对应的测试代码一起读了。测试用例能帮你弄清一个类的接口,和该类的典型用法。集成测试用例可以让你顺着走过程序的整体流程,适合输入一些特殊值并debug运行。
点评
为什么不在花了不少时间阅读一个项目后,写一篇代码点评呢?就像写一篇书评一样。你可以写下代码中好的和不好的部分,还可以记下你从中学到了什么。攥写这类文章可以帮助你阐明自己的理解,也有助于其他人阅读源码。