程序员构建总是出问题,怎么办

白癜风标本兼治 http://m.39.net/nk/a_6185580.html

构建这一问题,到底是哪个环节出了Bug?

作者

GiladBracha

译者

弯月,责编

屠敏

以下为译文:

我总是听到程序员谈论构建的问题:“构建出错了”,“我把构建搞坏了”等等。然而,真正的问题在于构建这个概念本身就有问题。每次修改应用程序都需要从头重新构建的想法从根本上就有缺陷。

这个概念的实际问题在于,构建会导致开发过程中的反馈循环漫长而痛苦。有些系统的应对手段是通过极快的速度,他们的观点是,哪怕你每次进行修改都编译整个系统,也不是问题,因为在你反应过来之前构建就会结束。

问题在于,系统的规模会一天天扩大,终有一天这个法子再也行不通。

更深层次的问题是,你会发现每次修改代码后,即使立即重新构建,还是需要重新启动应用程序,而且每当你发现一个问题、修改代码、经过重新编译后,问题还是没有消失。换句话说,构建与实时编程是对立的。构建的反馈循环永远都那么漫长。

从根本上讲,每次修改代码的时候,没必要重新编译整个系统。就好像你想换个灯泡也没必要把整座摩天大楼都拆了重建吧。

无论怎么优化,构建都无法真正成为实时。

因此,诸如make之类的工具永远也无法解决问题。而且这些工具本身还有许多其他问题。例如,我们必须复制代码中嵌入的依赖项信息:import、include、use或extern声明之类的语句为我们提供的文件和模块信息与我们手动输入并无二样。这些复制工作乏味且容易出错。而且这种复制太粗糙,粒度仅限于文件。编译器可以更精确地管理这些依赖关系,例如跟踪何处使用了哪些函数。

注意:有些工具(如GN)可以由编译器提供依赖关系。虽然仍然很粗糙。

另外,这些工具提供的语言所具备的抽象机制和工具支持都很差。一般大家对make的不满是其引入了其他类似性质的工具层,例如Cmake。

一个更好的解决方案是,为构建生成更好的DSL。基于某种真正的编程语言的内部DSL是改善问题的一种方法。例如rake和scons,分别使用了Ruby和Python。这些工具简化了构建工作,但它们仍与构建有着千丝万缕的联系,这才是我最关心的根本问题。

话虽如此,如果我们不打算使用传统的构建系统来管理我们的依赖项,那么应该怎么办?

首先,我们需要认识到我们的许多依赖关系不是基础的东西,例如可执行文件、共享库、目标文件和任意类型的二进制文件等。我们真正需要“构建”的只有源代码。毕竟,如果使用解释器,那么你只需创建最基本的源代码,然后逐步编辑和发展源代码。

使用解释器可以避免构建二进制文件的问题。

成本是性能。编译是一种优化,尽管是很重要的优化,通常是必不可少的。编译需要比解释器更全面的分析,而且还会执行预先计算,以避免我们在执行过程中重复工作。从某种意义上说,编译器的作用是记住解释器的部分工作。

许多动态JIT正是实现了这一点,但从根本上讲,静态编译也是如此——你只需提前记住即可。

从这个角度看,构建是分阶段执行的一种形式,而我们不断构建的二进制文件只是缓存。

我们可以通过结合解释与编译,解决解释器的性能难题。许多使用JIT编译器的系统正是这样做的。其优点之一在于,我们不必在启动应用程序之前等待优化。还有我们可以修改代码,并通过重新解释和重新优化立即反应修改的结果。

然而,并非所有的JIT都会这样做,但是这种做法已经延续了数十年,例如SmalltalkVM等。Smalltalk有很多优点,其中之一便是你很少会遇到构建的麻烦。

然而,即使假设你的JIT引擎在发展的过程中对代码进行了增量优化,你与实时开发之间仍有障碍,这个障碍就是你仍然需要构建。

类型。如果你的代码由于类型错误而出现问题,该怎么办?再次重申,我们不需要通过构建来检测到这一点。增量式类型检查器能够在保存代码时发现问题。当然,以往我们很少使用增量式类型检查器。实时系统的开发历来采用动态类型语言并非巧合。但是,没有根本性的原因能够说明为什么我们不能使用静态类语言支持增量开发。这些技术可以追溯到Cecil。有一篇有关Scala.js的文章(


转载请注明:http://www.aierlanlan.com/grrz/5138.html