关键时刻,第一时间送达!

我们的代码库混乱得一塌糊涂。所以有一天,我们决定做一些改进:也就是移动一堆文件。于是,我们花了两个月的时间完成了此次重构。

【编者按】本文作者Craig Silverstein 写了一系列文章来记录可汗学院在 2017 年和 2018 年对 Python 代码进行的重大重构,本文为其中第一篇。在本文中,作者介绍了代码在哪些地方出现问题,想要努力修正的内容,以及这项工作为何如此之困难,从而来帮助人们利用本文中介绍的技巧和代码来避免这些困难。

注:可汗学院(Khan Academy),是由孟加拉裔美国人萨尔曼·可汗创立的一家教育性非营利组织,主旨在于利用网络视频进行免费授课,现有关于数学、历史、金融、物理、化学、生物、天文学等科目的内容。

以下为正文:

我们的代码库混乱得一塌糊涂。所以有一天,我们决定做一些改进:也就是移动一堆文件。于是,我们花了两个月的时间完成了此次重构。

我们的代码库究竟有什么问题?可能包括:

  1. 有一些文件名为 parent.py,coaches/parent.py,users/parent.py 和 api/internal/parent.py,每个文件都包含了有关可汗学院学生家长的部分逻辑。
  2. 有一个名为 login.login.Login.login 的方法。 (这个方法是登录过程的一部分。)
  3. 根目录有 234 个 python 文件。超过 10%的代码保存在根目录。不看也知道,很多文件之间根本毫无关联。其中有 ip_util.py 等底层工具,也有 dismissed_items.py 等奇怪的特殊工具,还有 activity_summary.py 之类的应用程序逻辑文件,以及 appengine_stats.py 等开发工具。我们还有 93 个 Python 子目录。 (另外还有一些非 Python 代码。)虽然根目录被各种文件塞满,但是其中 30 个子目录仅包含不到 5 个文件。

经过重构后,与家长有关的代码仅保存在两个 Python 文件中。(一个是 API,另一个囊括了所有其它内容。)登录方法现在叫做 login.handlers.ka_login.login_and_set_current。并且 Python 子目录减少到了原来的一半,而根目录中的文件数量只有原来的 1/20。

这些问题都是如何发生的?

说的冠冕堂皇一点就是日积月累。大家在创建子目录的时候,以为那个目录会成为重大项目,但是事实却并非如此。而大家把新建的文件放在根目录下的原因是根目录下有个相似的文件。最终问题就一点点积累起来了。在找不到与手头的工作相关的已有代码时,我们一般都会在新文件中写新的代码。而当代码的组织变得毫无道理时(与公司当前的产品或与组织结构不相关时),大家找不到更好的地方保存新文件,所以就随便放了。没有人专门负责代码的结构,代码永远也得不到任何修整与重塑。

我们进行了哪些改善?

我们在 2017 年底预留了 6 周时间,让整个公司集中精力解决技术债务,因此我们 3 人决定利用这段时间来重塑代码库(Python 部分)。

注:我们只重构了 Python,因为 Javascript 代码已经组织得很好了。JS 代码组织得越好,用户的下载包就越紧凑,且用户体验越快。因此,JS 代码在提高性能的时候已被清理得很整齐,而 Python 代码还很乱。

我们从子目录着手。一位熟悉大部分代码库的高级工程师设计了初步的目录结构。其中一些用于 test_prep 或 translation 等产品。其他一些则用于 coaches 或 login 等主要数据结构和工作流程。还有一些用于邮件或 pubsub 等底层的基础设施。然后他们花了两天时间将代码库中的每个文件都转入了这些目录中。有几十个无法分类的文件,暂时标记为 TODO。

经过这次粗略的修整后,真正的工作才拉开序幕。另一个组重新浏览了代码库中的每个文件,并根据需要进行重新分类。在这个过程中,有些目录被删除了,有些新的目录加了进来,还有些目录被分割或合并了。经过更仔细的检查后,文件最终被转移到别的目录下。期间经常遇到文件被拆分的情况,因为它们保存了不相关的代码。(尤其是一些较大的文件被分到 4 个不同的目录中!)这项工作没有捷径,我们几乎把代码库中所有代码都看了一遍。

这一步中最有意思的事情是我们如何重新考虑代码库的一些部分。例如,以前我们的 emails 目录保存了可以向用户发送的所有类型的电子邮件。经过此次分析后,我们意识到如果 emails(现在改名为 email)中只包含邮件发送框架的话,代码会更整洁,并且每个具体的邮件应该与其最密切的代码保存在一起(比如 SAT 相关的邮件保存在 sat 中,新用户的邮件保存在 login,等等)。与之类似,我们将 API 处理程序从 api 目录中拿出来,放到各自相关的项目目录中。这两种组织方式各有利弊,但是之前我们想都没想就采用了前者。在重构过程中,我们重新考量了这两种方案。

在决定了各个文件的去向后,我们使用了 slicker 来实际移动文件。这个工具就像 mv,但是更加智能,不仅可以重命名文件,还可以自动修正所有的 imports 以及注释和字符串的引用,例如 mock.patch('path.to.symbol')。它还可以将函数和类从一个文件移到另一个文件,因此我们利用这个功能来分割文件。