为复杂项目做贡献
原文由 Mitchell Hashimoto 于 发布,订阅该博客
作为一名资深的开源维护者和贡献者,我经常被问到:该从哪里入手?如何着手一个新项目,并做出有意义的改动?又怎么可能搞懂一个复杂项目的内部原理?
这些问题适用于任何软件项目,无论它是开源还是闭源,是业余项目还是专业项目。我在任何情况下采取的方法都是一样的。不过,专业工作有一个关键区别:你可以直接接触到愿意——甚至有义务——帮助你的其他工程师,而在开源项目中,你大多只能靠自己。
我已经形成了一套应对复杂项目的固定方法,并在本文中将其记录下来。我不指望这套方法对所有人都适用,但希望它能帮助其他人建立信心,去尝试学习和参与复杂项目。
我用复杂项目这个词来指代任何实现原理无法一眼看懂的软件项目。这个定义是主观的;有些人觉得复杂的项目,另一些人可能不觉得,反之亦然。
第一步:先成为用户
理解任何项目内部原理的第一步,是先成为这个项目的使用者。你不必成为专家,但我个人判断完成这一步的标准是尝试用这个项目做出一个真正可用的东西,哪怕它很小、很简单。例如,在为 Zig 编程语言做贡献之前,我就创建了几 个 真实的 库。
作为用户,你会对项目的能力有一个整体的认识。阅读参考文档和在实践中使用项目有着天壤之别,而通过做一些练手项目,正是从理论理解迈向实践理解的重要桥梁。
此外,你还会开始了解项目的惯用法,这些惯用法构成了项目的文化底色,有助于你理解项目为何以这样的方式运作、为何具备这些功能等等。这一点很重要,因为它既能帮助你对参与项目的其他人产生共情,也能为你判断哪些改动适合或不适合这个项目提供指引。
在这个阶段,我也强烈建议加入社区。加入 IRC 或 Discord,参加本地聚会,观看相关演讲,等等。花一段时间多听少说。这里的目标是培养共情、了解项目的运作方式。仅仅通过观察他人如何学习,我就常常能学到很多,这一点总让我感到意外。
第二步:把项目构建起来
学会如何构建项目,并得到一个可运行的二进制文件(或等价物)。不用去纠结构建系统、依赖等等是怎么回事。照着指南、网站上的说明生搬硬套就行,只要能让你在自己的机器上稳定、反复地从源代码构建出可运行的二进制文件即可。
在学会构建项目之前,不要去读代码。我经常看到有人还没学会如何构建项目,就陷在试图理解源代码上。对我而言,学习过程的一部分就是去尝试、去弄坏点东西,而如果连构建都不会,就很难对软件项目进行尝试和破坏。
不用追求功能完整的构建。复杂项目往往有些功能只有在具备正确的依赖、正确的系统、正确的配置等条件下才可用。如果是这种情况,完全不用在意这些。目标是在你的系统上得到一个基本可用的二进制文件。随着你进入后面的步骤,你会积累经验和信心,再去追求更完整的功能构建。
在这一步,我还建议你学会如何运行测试套件并让它通过。这会让你在后面的步骤中更容易进行尝试和破坏。复杂项目往往也有复杂的测试套件,所以有时这意味着只需要让测试套件中的一部分跑起来——只要够你做实验就行。
第三步:学习热点路径的内部原理
要学习内部原理,我喜欢用一种我称之为“自上而下追踪,自下而上学习”的方法。
自上而下追踪
我从某个功能或用例入手,从外到内追踪该功能所经过的代码路径。在这个过程中,我会记下所经过的文件、行号和函数,但暂时不去尝试理解任何部分的运作原理。这就是“自上而下追踪”阶段。
例如,在研究 Zig 编译器时,我从追踪 zig build-exe 命令开始,该命令用于从 Zig 源代码构建可执行文件。追踪过程让我找到了 zig 命令行工具的源码、build-exe 子命令,进而进入“Compilation”子系统,该子系统随后会调用词法分析器、语法分析器等。我没有去阅读超出追踪路径所需的实现细节。
通过追踪笔记,你通常就能对某个功能的运作方式有一个“全局概览”。根据文件名、函数名等,你通常可以开始分辨出项目的主要子系统。这有助于在后续将学习过程拆分成更合理、更易消化的小块。
不要试图一次性学会所有东西。我常见的一个错误是,人们试图逐行读完整 个项目,结果迷失数周甚至数月,最终感到气馁。要保持专注,一次只学一个功能。
提示:在挑选功能时,选择一个你作为用户已经熟悉的功能。另外,如果可能,尽量选一个表面上看起来很简单的功能。例如,为了学习编译器,我追踪的第一个 Zig 程序就是一个只做两数相加且没有任何输出的程序。
自下而上学习
追踪完一个功能后,就该真正去学习各个已梳理出的子系统是如何工作的。与追踪阶段从最外层的 CLI 或 API 调用开始不同,在学习阶段,我倾向于从最内层开始。
我选择从最内层开始,是因为它通常是最基础、抽象程度最低的。随着你逐层向上,抽象层级往往会越来越高,如果不理解底层的组件,就更难学懂上层。
要开始学习某个特定子系统,我会递归地“自上而下追踪,自下而上学习”。我会先查看其公开、导出的 API 接口,然后学习每个 API 调用是如何工作的。这正是上层如何使用该子系统的方式,因此这既为我如何学习提供了指引,也会在我向上深入技术栈时让一切变得更清晰。
动手实验,大胆试错
在“自上而下追踪,自下而上学习”的过程中,我发现通过动手实验、甚至故意弄坏点东西来学习其工作原理非常有帮助。这也是为什么在尝试阅读内部实现之前,一定要先学会如何构建项目。
添加新的日志语句、实现一小块新功能、修改现有功能等等,然后重新构建项目,看看会发生什么。这也是真正检验你是否理解某个工作原理的好方法。
例如,在学习 Zig 的词法分析器时,我添加了新的词法单元,看到它们能够被正确分词,但随后发现解析器报错了。当我学到下一个系统(解析器)时,我就让这些新词法单元真正起作用。以此类推。
借助外部资料补充学习
在整个阶段,用各种可获取的资料来补充你在代码细节中的探索:书籍、视频、博客文章等等。如果已有介绍内部原理的文献,一定要去读!
不过,不要指望仅靠这些资料就能让你达到专家的水平。和第一步“成为用户”类似,在尝试“成为维护者”时,没有任何东西可以替代亲手摆弄实际源代码的实践。这又是理论与实践之别的体现。
提示:如果不存在学习内部原理的资料,试着自己动手写一份!我就是这么做的——因为找不到类似的最新资料,我写了关于Zig 编译器内部原理的文章。写作是巩固学习的好方法,也能帮助未来的贡献者。
第四步:阅读并复现最近的提交
作为学习内部原理的最后一步,我会阅读与我已研究过的子系统相关的最近提交,并检验自己是否完全理解做出这些改动的原因。这相当于我学习过程中的“做课后习题”环节。
我会查看整个项目的提交历史,或是与某个子系统相关的特定文件或文件夹的提交历史。然后,我要么先研究解决方案(提交中的改动),要么先看它修复的 bug,并尝试自己去修复,看看能否得出类似的解决方案。
为了“解题”,我会将仓库切换到我正在研究的那个提交的前一个提交。我会复现它所修复的 bug(如果是 bug 修复),然后尝试自己实现解决方案。最后,我会将自己的成果与维护者或贡献者的提交进行对比。
我给自己的唯一提示是所需的改动规模(版本控制系统 diff 中的 +/- 行数)。我建议一开始避免涉及超过 50 到 100 行改动的提交。
第五步:做一个小而具体的改动
我喜欢从小处着手,逐步承担越来越大的任务。到了这个阶段,你已经理解了项目的技术组成部分,接下来就要去了解其中的人的部分。这里的目标是做一个小改动,并熟悉贡献和代码审查的流程。
最难的部分通常是找到一个适合的小改动。这里没有一劳永逸的办法。我会浏览 issue 列表,寻找看起来对新贡献者友好的任务。我通常会经历几次错误的尝试,或是彻底放弃某个 issue 转而尝试其他。最终,总会找到一个。寻找 issue 甚至修复 issue 所花费的时间可能会长得让人沮丧,但这就是入门的代价。许多项目都会打上“适合新贡献者”之类的标签来引导新人。
如今大多数项目都会很好地记录其贡献流程,所以一旦完成改动,就严格按照流程来操作。如果你已经在前面的步骤中加入了社区,这就是一个很好的机会去联系某个人寻求帮助,或请人帮你检查一下流程是否正确。
举个例子,我对 Zig 的第一次贡献是一个三行的改动,花了我两个晚上、总计四五个小时才完成(还不包括前面几个步骤所花的时间)。如果这个 bug 今天再次出现,我几分钟内就能修好,但要达到这种熟练程度是需要时间的。
成功
到这里,你已经学会了一个复杂项目,并成功做出了贡献!
不要畏惧复杂。我觉得太多工程师把编程语言、浏览器、数据库等典型意义上的复杂项目看作是魔法,或是只有高人才能企及的东西。我喜欢提醒自己,所有的项目都是由其他人从零开始做起的。如果他们能做到,我也能做到。你也一样可以。
希望通过分享我的方法,能让其他人觉得复杂项目不再那么难以接近。
随机一篇博客
评论
登录后参与讨论