Contributing to Complex Projects

Mitchell Hashimoto

为复杂项目做贡献

作为一名频繁维护和贡献开源项目的人,我经常被问到:你从哪里开始?如何着手一个新项目并做出有意义的改动?又怎么可能理解一个复杂项目的内部实现?

这些问题适用于任何软件项目,无论它是开源还是闭源,是业余爱好还是专业项目。我在任何情况下采取的方法都是一样的。不过,专业工作中的一个关键区别在于,你可以直接接触到愿意——甚至有义务——帮助你的其他工程师,而在开源项目中,你大多只能靠自己。

我已经形成了一套应对复杂项目的固定模式,并在本文中将其记录下来。我并不指望这种模式对每个人都有效,但希望它能帮助其他人建立信心,去尝试学习和参与复杂项目。

我用复杂项目一词来指代任何实现并不容易理解的软件项目。这个定义是主观的;有些人认为某个项目很复杂,而另一些人则不这么认为,反之亦然。

步骤一:成为用户

理解任何项目内部实现的第一步是成为该项目的使用者。你不必成为专家用户,但我个人对这一步的完成标准是尝试用该项目构建一个真实的东西,即使它很小或很简单。例如,在为Zig 编程语言做贡献之前,我创建了真实的

作为用户,你将对项目的能力有广泛的了解。阅读参考文档与在实践中使用项目之间存在着鲜明的差异,而创建一些小项目是从理论理解迈向实践理解的重要一步。

此外,你将开始学习项目的惯用法,这些惯用法构成了项目的文化底色,并有助于引导你理解项目为何以这种方式运作、为何具备这些功能等等。这一点很重要,因为它有助于培养对项目中其他参与者的共情,同时也能为哪些改动适合或不适合该项目提供指引。

我也强烈建议在这一阶段加入社区。加入 IRC 或 Discord,参加本地聚会,观看演讲等等。在一段时间内多听少说。这里的目标是培养共情并学习项目是如何运作的。我总是惊讶于仅仅通过观察他人学习,自己就能学到这么多。

步骤二:构建项目

学习如何构建项目并得到一个可运行的二进制文件(或等价物)。不必费心去理解构建系统、依赖等。只需生搬硬套地照做指南、网站等任何能让你在自己的系统上可靠且可重复地从源代码得到可运行二进制文件的方法。

在学会构建项目之前不要去读代码。我经常看到人们在学会如何构建项目之前,就陷入试图理解项目源代码的困境。对我而言,学习过程的一部分就是尝试和破坏,而如果无法构建项目,就很难对软件项目进行尝试和破坏。

不必担心功能完整的构建。复杂项目往往有一些功能只有在具备正确的依赖、正确的系统、正确的配置等条件下才可用。如果是这种情况,完全不必担心这些。目标是在你的系统上得到一个足够好用的二进制文件。随着你进入后面的步骤,你会积累经验和信心,再去追求更功能完整的构建。

在这一步中,我还建议学习如何运行测试套件并使其通过。这会让在后续步骤中进行尝试和破坏变得更容易。复杂项目往往也有复杂的测试套件,因此有时这意味着只需让测试套件的子集跑起来——只要足够用于试验即可。

步骤三:学习核心路径的内部实现

为了学习内部实现,我喜欢使用一种我称之为“自上而下追踪,自下而上学习”的方法。

自上而下追踪

我从一个功能或用例入手,从外到内追踪该功能所经过的代码路径。在此过程中,我会记录所经过的文件、行号和函数,但暂时不会尝试去理解任何东西是如何工作的。这就是“自上而下追踪”阶段。

例如,在研究 Zig 编译器时,我从追踪 zig build-exe 命令开始,该命令用于从 Zig 源代码构建可执行文件。这一追踪过程让我找到了 zig CLI 的源码、build-exe 子命令,进而进入“Compilation”子系统,该子系统随后会调用词法分析器、语法分析器等。除了追踪路径所需的内容外,我没有去阅读实现细节。

通过追踪笔记,你通常可以对某项功能的运作方式获得一个“全局观”。基于文件名、函数等,你通常可以开始辨别项目的主要子系统。这有助于在之后将学习过程划分为更合理大小的块。

不要试图学习所有东西。我常见的一个错误是人们花上数周或数月迷失其中,最终感到气馁,因为他们试图逐行读完整 个项目。要保持专注,逐个功能地学习。

提示:在挑选功能时,选择一个你作为用户比较熟悉的功能。另外,如果可能,尽量挑选一个表面上看起来简单的功能。例如,我为学习编译器而尝试追踪的第一个 Zig 程序是一个只做两数相加且没有任何输出的程序。

自下而上学习

在追踪完一个功能后,就该真正学习各个已映射子系统是如何工作的了。在追踪阶段,我从最外层如 CLI 或 API 调用开始,而在学习阶段,我则倾向于从最内层开始。

我选择从最内层开始,因为它通常是最基础、抽象程度最低的。随着你逐层向上,抽象层次往往会不断提高,如果不理解组成部分,就更难学习。

要开始学习某个特定子系统,我会递归地“自上而下追踪,自下而上学习”。我会先查看公开、导出的 API 界面,然后学习每个 API 调用是如何工作的。更高层的代码就是这样使用该子系统的,所以这既为我如何学习提供了指引,也会在我沿着技术栈向上推进时让一切变得更清晰。

尝试与破坏

在“自上而下追踪,自下而上学习”的过程中,我发现通过尝试和破坏来学习某项功能如何工作非常有帮助。这就是为什么在尝试阅读内部实现之前学会如何构建项目至关重要。

添加新的日志语句、实现一小块新功能、修改现有功能等,然后重新构建项目并观察会发生什么。这也是真正检验你对某项功能理解程度的好方法。

例如,在学习 Zig 词法分析器时,我添加了新的 token,发现它们能够被正确分词,但随后发现解析器失败了。当我进入下一个系统(解析器)时,我让新 token 真正发挥作用。以此类推。

借助媒体资料补充学习

在整个阶段中,用任何可用的媒体资料来补充深入代码的探索:书籍、视频、博客文章等。如果有涵盖内部实现的现有文献,一定要去读!

不过,你不应指望仅靠这些资料就能让你达到专家水平。与步骤一中“成为用户”类似,在尝试“成为维护者”时,没有任何东西可以替代亲自动手摆弄实际源代码。这是理论与实践应用的又一个例子。

提示:如果不存在学习内部实现的资源,试着自己来写!我就是这样做的,我写了关于Zig 编译器内部实现的文章,因为我找不到类似的最新资源。撰写相关内容是巩固学习的好方法,也能帮助未来的贡献者。

步骤四:阅读并重新实现最近的提交

作为学习内部实现的最后一步,我会阅读与我所研究子系统相关的最近提交,并检验自己是否完全理解做出该改动的原因。这是我学习中“做课后习题”的部分。

我会查看项目的提交历史,或与我所研究子系统相关的特定文件或文件夹的提交历史。然后,我要么先研究解决方案(提交中的改动),要么先查看它所修复的错误并尝试自己修复,看看能否得出类似的解决方案。

为了“解题”,我会将仓库检出到我正在研究的那个提交的前一个提交。我会复现它所修复的错误(如果它是错误修复),然后尝试自己实现解决方案。最后,我会将自己的成果与维护者或贡献者的提交进行对比。

我给自己的唯一提示是所需的改动规模(VCS diff 上的 +/- 行数)。我建议一开始避免那些需要改动超过 50 到 100 行的提交。

步骤五:做一个小而可控的改动

我喜欢从小处着手,逐步承担越来越大的任务。到了这个阶段,你已经理解了项目的技术组成部分,现在是时候了解人的组成部分了。这里的目标是做一个小改动并学习贡献和审查流程。

最难的部分通常是找到一个可做的小改动。这里没有灵丹妙药。我会浏览 issues,寻找看起来对贡献者友好的内容。我通常会经历几次错误的开始,或彻底放弃某个 issue 转而尝试其他。最终,我会找到一个。寻找 issue 甚至修复 issue 所花费的时间可能会长得令人沮丧,但这是入场的代价。项目通常会有“对贡献者友好”的标签来引导新贡献者。

如今大多数项目都会很好地记录其贡献流程,所以一旦实现了改动,就严格按照流程来。如果你已经在前面的步骤中加入了社区,这就是一个好机会去联系某人寻求帮助,或请人复核你的流程。

举个例子,我对 Zig 的第一次贡献是一个三行改动,花了我两个晚上大约四五个小时才完成(不包括前面步骤所花的时间)。如果这个错误今天再次出现,我几分钟内就能修复,但要达到那种熟练程度需要时间。

成功

到此为止,你已经学会了一个复杂项目并成功做出了贡献!

不要害怕复杂性。我认为太多工程师将那些典型意义上复杂的项目,如编程语言、浏览器、数据库等,视为魔法或只有高人才能企及的东西。我喜欢提醒自己,所有项目都是由其他人创建的。如果他们能做到,我也能做到。你也一样可以。

希望通过分享我的方法,能让其他人觉得复杂项目更平易近人。

原文由 Mitchell Hashimoto 发布

本文章由 muse-spark-1.2-contributor 进行翻译