最近后台有个同学问阅读源代码的问题。他说感觉自己团队维护的代码都看不过来,还需要看开源社区的代码么?
我的回答是:一定要。两个原因:1) 团队维护的代码受限于团队的水平,构建过程中的各种赶工,以及不断堆叠的业务逻辑,质量可能不高;2) 因为架构模型,设计模式以及业务范围的束缚,如果只了解团队内部的代码,那将会是井底之蛙,眼界非常狭窄。
要不要阅读源码,以及如何阅读源码是个很普遍的问题,今天就展开讲讲。
为什么我们要阅读源码?
程序员每天都和代码打交道。经过数年的基础教育和职业培训,大部分程序员都会「写」代码,或者至少会抄代码和改代码。但是,会读代码的并不在多数,会读代码又真正读懂一些大项目的源码的,少之又少。这种怪状,真要追究起来,怪不得程序员这个群体本身 —— 它是两个原因造成的:
- 我们所有的教育和培训都在强调怎么写代码,并没有教大家如何读代码
- 大多数工作场景都是一个萝卜一个坑,我们只需要了解一个系统的局部便能开展工作,读不相干的代码,似乎没用
我常常把写代码和写作进行类比 —— 二者有很多相通之处;但从培养写代码和写作的过程来看,二者又有很多不同。我们的写作能力,是建立在大量基础阅读的基础上的,是除了学习语法和文法知识外,从小学开始,经年累月,通过阅读各种不同层次的名家的作品,再加上各种各样的写作训练,累积出来的;而我们的写代码的能力,在了解和掌握了语法/文法之后(学习和抄写 example 代码也算语法/文法学习的一部分),跳过了大量阅读名家作品的过程,直接 biu 地一下就自动养成了:学会基础的语法和试验了若干 example 后,我们就火箭般蹿到了自己写代码打怪攒经验的阶段。略过了大量阅读代码的阶段有三个害处:
1. 写代码的基础是不牢靠的,打怪升级的过程也是最慢的。道理很简单 —— 前辈们踩过的坑,总结的经验教训,你都不得不亲自用最慢的法子一点点试着踩一遍。
2. 很容易养成 stackoverflow driven 的写代码习惯 —— 遇到不知如何写的代码,从网上找现成的答案,找个高票的复制粘贴改吧改吧,凑活着完成功能再说。写代码的过程中遇到问题,开启调试模式,要么设置无数断点一步步跟踪,要么到处打印信息试图为满是窟窿的代码打上补丁,导致整个写代码的过程是一部调代码的血泪史。(见我的文章:)
3. 编码水平的上限就是你周围最强的那个工程师的上限。
我们再回到读书。
从小学到高中,就语文而言,12年时光,单单课本我们要读十二册,数百篇文章。如果每篇文章平均一千字,那么我们读了数十万文字。这些文字,我们是精读过的(有些甚至要求全文背诵)。每篇文章我们需要总结中心思想,段落大意,归纳出论点论证论据或者时间任务地点起因经过结果,会分析长句难句,会学习起承转结,并反复训练基础的遣词造句能力,并最终模仿那些文章写出自己的文章。在这个过程中,我们学会了赋、比、兴,我们掌握了三段论,我们知道了如何用更优雅的方式表达自己的思想。更重要的是,这些阅读训练让我们在我们在脱离学校的基础教育后,可以自己独立完成一本书的阅读。我们不再依赖老师或者参考书为我们给出段落大意,中心思想,我们知道如何粗读,细读甚至类比阅读一本书,我们能把书中的精髓浓缩成思维导图,也大段大段摘录书中精彩的句子,段落或者篇章。
这十几年的时光,算上各种课外阅读,世界名著,古典文学,金庸古龙,修真玄幻,一个大学毕业的二十几岁的青年人,阅读量应该不下几百万字。而稍稍涉猎广些的读者,上千万字的阅读累积是常有的事。有了这些累积,你才能在迎面走来一位妙龄女子,想到的是肌肤胜雪,明眸善睐,桃腮带笑,齿如含贝,气若幽兰,美艳不可方物,一笑倾城,再笑倾国,沉鱼落雁,闭月羞花这些词句,而非不知如何表达,只能吞吞口水,在肚子里闷上一句:「我擦,美女」。
这是读书的第一大功用:累积素材(information)。你是否写文章时,经过一番搜肠刮肚,也不知该如何描述某事某物?同样的,写代码时,有没有毫无头绪,不知从何写起的时候?或者有了写作的思路,双手却在键盘上迟滞,不知所措?这些现象,大多是缺乏累积所致。
读书的第二大功用:开拓思路。有时候,一段文字,甚至一个句子,在你意料之外扑面而来,让你有种醍醐灌顶的感觉。比如『围城』里,赵辛楣和方鸿渐鸿初次见面,钱老描述赵的傲慢无礼,是这么写的:「傲兀地把他从头到脚看一下,好像鸿渐是本一览而尽的大字幼稚园读本」。初读围城的时候,我关注点是其故事性,将这样的句子轻易放了过去,几年前再读时,才发觉它的精妙:竟能如此简单地以物喻人,就把整个场景复原到如同发生在我的面前一样活灵活现。随后,我自己的文字里也模仿着,有时甚至刻意地如此这般使用比喻来增强画面感。前些日子偶尔再读到这句,因我有了给女儿读幼稚园读本的经验,不由得莞尔一笑,旋即明白了一个道路:精妙的不是比喻本身,而是对生活的细微观察。
上个月,我用 Rust 写了个几千行的小项目 wormhole,做了一个高度可配置和可定制(集成了 rhai 脚本)的 proxy 服务器。其中,内部的很多 trait 的设计,都源自于我从 tower,axum,nom 等项目源码中学到的经验。
有时候,你读源码受到的启发可能一下子不会找到使用的场景,就像我上一篇文章中介绍的 axum 如何使用声明宏 泛型 trait 构建出可以媲美动态语言的 API 使用感受,在惊叹之余,很可能自己手头的项目并不能很快找到这样的精妙设计的用武之地。但是不要担心。就像乔老爷子说的那样,珠子总有一天会被串起来的。就像张无忌小时候在冰火岛背诵的那些心法口诀,学的时候固然枯燥,但时机到了,小时候摇头晃脑背下的那些篇章就能涌现出来。我在 wormhole 里 data plane 构建的 pipeline 的思路,就要源自数年前,我在读 elixir plug 源码的时候,学习到的思想。
累积素材是基础,被启发出来的思路将这些素材串成线,这就形成了知识(knowledge)。书读得越多,越勤于思考的人,知识也就越丰富。而知识的融会贯通,最终形成读书的第三大功用:通过了解,吸收别人的思想,去芜存菁,最终形成自己的思想,或者说智慧(wisdom)。
information -> knowledge -> wisdom 是个长期的累积,并非一朝一夕之功。阅读名家的优秀的代码最终的归宿是形成你自己写代码的思路。
如何阅读开源社区的代码呢?
如果你接受了上面的「一定要尽可能多的阅读优秀的源码,就像阅读优秀的书籍那样」的观点,那么下一个问题自然是:如何阅读别人的代码?
首先是要学会甄选合适的代码库。这就跟我们选择合适的书去阅读一样。一个还不具备高级阅读能力的人去啃 linux kernel,必然会栽个大跟头。在我看来,和工作相关的,在工作中用到的开源项目的依赖,是我们需要优先去阅读理解的内容。我在使用 Rust 的时候,基本上,用到的 crates,我都会去扫一扫源代码。
我阅读的顺序是这个样子的:首先,看项目的 readme 和发布的文档。如果一份代码没有高质量的 readme 或文档,那么就不着急细看,在使用时,把遇到的接口的定义,以及这样定义背后的逻辑搞清楚就好。
如果项目的文档不错,那么,可以顺着文档找自己感兴趣的地方,进一步了解这个项目。我很喜欢 rustdoc 的一个很大的原因,就是文档和代码,尤其是接口代码的关联度很好,可以来回跳转,缩短我阅读的时间。如果我感觉代码的质量不错,某些设计激起了我的兴致,那么我会走到第三步:git clone,把代码复制到本地细细品味。
git clone 的好处是,你可以摆脱网页跳转的种种束缚,在你自己钟爱的编辑器中阅读代码。此刻,代码的细节就一览无余,很容易陷入到细节的纷扰中。我们在阅读书籍的时候,是一个线性的过程,一般读完一个章节再到下一个章节,很少有在章节之间反复跳跃的。但阅读代码的时候,如果不注意抓住主线,那就会陷入无穷尽的跳转之中,在代码的汪洋中迷失掉。所以,我会围绕着具体的场景,每个场景一条主线,尽可能控制分支地阅读。阅读的过程以接口的设计,接口和接口之间的交互为主,并且随时准备好 excalidraw 绘制当前场景的流程,所以,一个场景的代码阅读得差不多,这个场景下的流程图也出来了。这样的阅读方式,可以快速掌握代码的结构。
大部分同学估计工作都很忙,能把工作过程中遇到的依赖一个个啃下来就不错了。当然,肯定还有学有余力的同学。如果工作中遇到的值得读的开源依赖都搞定了,还能读些什么呢?
我的建议是围绕着你做的产品的整个架构,寻找这个架构中你不了解,但又感兴趣的部分,找对应的优秀的开源项目去学习。比如作为一个后端工程师,你可能维护一个内部的管理系统的 REST API。当你有余力的时候,可以从一个 API 的生命周期中遇到的所有场景中,选择你感兴趣的内容。API 离不开网络,那么,自然,相关的网络协议值得了解,对于 Rust 而言,涉及到的开源代码有 tokio,hyper,甚至 quinn(http3)。网络之上是安全,安全协议及相关的算法,就足够折腾很久了,比如处理 TLS 的 rustls,处理 noise protocol 的 snow,以及它们背后的哈希算法,对称和非对称加密算法等等。安全之上是授权,oso / casbin 都可以看看。网络传输过程中涉及到序列化反序列化,那么在 serde / serde-json 的基础上,是不是还可以再研究一些其它的序列化反序列化方案,着重了解一下它们的使用场景和优劣?再往后,数据的存储,缓存的方案,系统的 telemetry 的收集(以及整个 OpenTelemetry 体系的架构)等等,一个小小的 API 就可以让你有足够的广度和深度去探索,更别说从整个产品的角度去探索。