track image
为什么我们要阅读源码?

为什么我们要阅读源码?

陈天陈天

程序员每天都和代码打交道。经过数年的基础教育和职业培训,大部分程序员都会「写」代码,或者至少会抄代码和改代码。但是,会读代码的并不在多数,会读代码又真正读懂一些大项目的源码的,少之又少。这种怪状,真要追究起来,怪不得程序员这个群体本身 —— 它是两个原因造成的:

  • 我们所有的教育和培训都在强调怎么写代码,并没有教大家如何读代码

  • 大多数工作场景都是一个萝卜一个坑,我们只需要了解一个系统的局部便能开展工作,读不相干的代码,似乎没用

我常常把写代码和写作进行类比 —— 二者有很多相通之处;但从培养写代码和写作的过程来看,二者又有很多不同。我们的写作能力,是建立在大量基础阅读的基础上的,是除了学习语法和文法知识外,从小学开始,经年累月,通过阅读各种不同层次的名家的作品,再加上各种各样的写作训练,累积出来的;而我们的写代码的能力,在了解和掌握了语法/文法之后(学习和抄写 example 代码也算语法/文法学习的一部分),跳过了大量阅读名家作品的过程,直接 biu 地一下就自动养成了:学会基础的语法和试验了若干 example 后,我们就火箭般蹿到了自己写代码打怪赞经验的阶段。这样略过大量阅读代码的阶段有三个害处:

  1. 写代码的基础是不牢靠的,打怪升级的过程也是最慢的。道理很简单 —— 前辈们踩过的坑,总结的经验教训,你都不得不亲自用最慢的法子一点点试着踩一遍。

  2. 很容易养成 stackoverflow driven 的写代码习惯 —— 遇到不知如何写的代码,从网上找现成的答案,找个高票的复制粘贴改吧改吧,凑活着完成功能再说。写代码的过程中遇到问题,开启调试模式,要么设置无数断点一步步跟踪,要么到处打印信息试图为满是窟窿的代码打上补丁,导致整个写代码的过程是一部调代码的血泪史。(见我的文章:你要避免的软件开发模式

  3. 你周围最强的那个工程师的开发水平的上限就是你的上限。

我们再回到读书进行类比。

从小学到高中,就语文而言,12年时光,单单课本我们要读十二册,数百篇文章。如果每篇文章平均一千字,那么我们读了数十万文字。这些文字,我们是精读过的(有些甚至要求全文背诵)。每篇文章我们需要总结中心思想,段落大意,归纳出论点论证论据或者时间任务地点起因经过结果,会分析长句难句,会学习起承转结,并反复训练基础的遣词造句能力,并最终模仿那些文章写出自己的文章。在这个过程中,我们学会了赋、比、兴,我们掌握了三段论,我们知道了如何用更优雅地方式表达自己的思想。更重要的是,这些阅读训练让我们在我们在脱离学校的基础教育后,可以自己独立完成一本书的阅读。我们不再依赖老师或者参考书为我们给出段落大意,中心思想,我们知道如何粗读,细读甚至类比阅读一本书,我们能把书中的精髓浓缩成思维导图,也大段大段摘录书中精彩的句子,段落或者篇章。

这十几年的时光,算上各种课外阅读,世界名著,古典文学,金庸古龙,修正玄幻,一个大学毕业的二十几岁的青年人,阅读量应该不下几百万字。而稍稍涉猎广些的读者,上千万字的阅读累积是常有的事。有了这些累积,你才能在迎面走来一位妙龄女子,想到的是肌肤胜雪,明眸善睐,桃腮带笑,齿如含贝,气若幽兰,美艳不可方物,一笑倾城,再笑倾国,沉鱼落雁,闭月羞花这些词句,而非不知如何表达,只能吞吞口水,在肚子里闷上一句:「我擦,美女啊」。

这是读书的第一大功用:累积素材(information)。你是否写文章时,经过一番搜肠刮肚,也不知该如何描述某事某物?同样的,写代码时,有没有毫无头绪,不知从何写起的时候?或者有了些的思路,双手却在键盘上迟滞,不知所措?这些现象,大多是缺乏累积所致。

读书的第二大功用:是开拓思路。有时候,一段文字,甚至一个句子,在你意料之外扑面而来,让你有种醍醐灌顶的感觉。比如『围城』里,赵辛楣和方鸿渐鸿初次见面,钱老描述赵的傲慢无礼,是这么写的:「傲兀地把他从头到脚看一下,好像鸿渐是本一览而尽的大字幼稚园读本」。初读围城的时候,我关注点是其故事性,将这样的句子轻易放了过去,几年前再读时,才发觉它的精妙:竟能如此简单地以物喻人,就把整个场景复原到如同发生在我的面前一样活灵活现。随后,我自己的文字里也模仿着,有时甚至刻意地如此这般使用比喻来增强画面感。前些日子偶尔再读到这句,因我有了作为一个成人,给女儿读幼稚园读本的经验,不由得莞尔一笑,旋即明白了一个道路:精妙的不是比喻本身,而是对生活的细微观察。

我在边学 elixir 边做 policy engine(见:Policy Engine 的前世今生)的过程中,除了官方的文档和零星的博文外,可读的内容少得可怜,我要解决的一些问题,论坛里也没人能给我较好的思路。于是我转而读了部分 iex 的代码,了解了 elixir 代码编译的方式,最终完成了一个 auto compiler 的 app —— 它能接受一些 API 请求,对预先配置的属于其他 app 下的源码可以在 cluster 里的有且仅有一个 node 上进行编译,编译完成后在整个 cluster 的所有 node 里重新加载;在做 API 的过程中,我读了 plug(elixir 下官方的 connection adapter 实现)里面的主要逻辑,尤其是精读了 Plug.Router 的代码,搞明白了为何 Phoenix 的 router 敢宣称在 route match 阶段,其 performance 就甩同行好几个数量级。于是我做 API 时,对如何在 match / dispatch 前后如何做些动作实现 middleware,甚至 hook 进 before send,有了更清晰的思路,在写代码时,也更加明白如何写出类似的 composable adapter。

累积素材是基础,被启发出来的思路将这些素材串成线,这就形成了知识(knowledge)。书读得越多,越勤于思考的人,知识也就越丰富。而知识的融会贯通,最终形成读书的第三大功用:通过了解,吸收别人的思想,去芜存菁,最终形成自己的思想,或者说智慧(wisdom)。

information -> knowledge -> wisdom 是个长期的累积,并非一朝一夕之功。

我的文章除几篇广告外,全部是原创,而原创中 99% 是我自己原创。这些原创完完全全是我「原创」么?我在 『如何选择工作』一文中借用了『黑客与画家』的思想:可测量性和可放大性。我赞同这个思想,同时将其融入了我自己的思考;在『程序员和拉条子』一文,我其实是写了个现代版的庖丁解牛;『代码重构之道』我借用了松本行弘,Martin Fowler,荀子等人的思想,形成了我自己的见解。当然,我书还是读得太少,所以,真正有价值的思想还贫瘠得很。

同样的,阅读名家的优秀的代码最终的归宿是形成你自己写代码的思路。我去年撰写的一个基于 node restify 的 API framework(见:再谈 API 的撰写 - 总览 系列),虽然有很多不完善的地方,但内核还是相当稳定,一年多来只是些许小修小补。撰写它的过程,是我对之前所读各种项目的一个融会贯通:为了确定我是否该基于 restify,我通读了它的代码(没多少),融合进了 hapi 和 loopback 的一些思路,提供了一套类似于 rails 的 CLI,再加上早年在 parser 上的一点经验,把 framework 逻辑上分出了:compile time(严格讲 nodejs 没有 compile time,但 configuration 的处理,route / middleware / model / task 对应的内部数据结构的生成,算进了 compile time 中),load time 和 run time,用前两个阶段的「低效」换取后一个阶段的高效(其实也类比了我的老本行,路由器/防火墙上的 control plane / data plane,first path / fast path 的设计)。如果没有之前各种源码阅读的累积,我很难做出这样的设计。

同样的,阅读 elixir unicode 实现的过程在两年前激发我做了一个使用同样思路的汉字的 slugify,或者汉字转拼音(解决多音字问题)的暴力方法(see github: chinese_translation),这一思路在今年年初进一步发扬光大成了我们现在在 Tubi TV 使用的 policy engine 和 content engine,为每日不计其数的内容请求保驾护航。我们使用了 code as cache 的方法,把数据库里的内容的不必在运行时做的 transformation 都在 compile time 完成,然后在 run time 结合记叙文三要素,得到最终的结果。以前我们系统中我们最慢的 API,采用新的 engine,如今快了至少数十倍 —— 尤其是在低并发下就惨不忍睹的 95 percentile response time(高并发都无法完成测试),如今在高并发,没有任何 failure 的情况下,99 percentile response time 都能控制在 500ms 左右。

所以你说,阅读优秀的,有启发性的源代码,多重要?

解决了 why 的问题,过两天我们讲讲 how。

(PS:最近天气转热,从冬天直接跳到夏天,所以容易便秘 —— 一不小心一篇文章憋出了两篇 ^_^)

文章被以下专栏收录
59 条评论
推荐阅读