作者: 周思博 (Joel Spolsky)
译: Chen Bin
2001年1月27日
1982年,我家定购了IBM的PC机(IBM生产的最早的个人计算机,是现代流行的标准化的个人计算机的祖宗)。我们家可能是以色列最早拥有这种PC机的几个家庭之一。当时我们跑到了仓库等着电脑从港口运过来。事实上,我说服我爸购买的是带有全套附属设备的个人计算机(译者按:有些现在很便宜的附属设备,那时候都是非常昂贵的),这些附属设备包括两个软盘驱动器,128K内存,一个针式点阵打印机(用来快速打印草稿)和一个运转起来发出机关枪扫射声音的兄弟牌的雏菊轮式打印机(译者按:原文为Daisy Wheel printer,是一种已经淘汰的打印机,原理类似于老式的机械打字机,可以产生清晰的英文字符)。
附属的软件也很齐全,PC-DOS 1.0(最早的PC操作系统),75美元的参考书册,包括BIOS的完整源代码。 一个汇编语言编译器(Macro Assembler),非常棒的IBM单色显示器,可以显示80列宽的字符,而且支持小写字母显示。整套配置大概花了10000美元。这些钱包括以色列的荒谬的进口税。呵呵,那时候我家可真舍得花钱啊!因为当时“每个人”都知道BASIC是给小孩玩的语言,用这种语言只能使你写出非结构化的垃圾代码,然后你的的脑子也会被这种语言变成Camembert产的奶酪(Camembert cheese,法国的一种奶酪,实心,圆饼状,灰色,有一个拳头大小)。所以我们又花了600美元买了一个IBM公司PASCAL语言开发包,需要3张软盘才装的下。PASCAL编译器运行分别需要第一号软盘,和第二号软盘,PASCAL链接器需要第三号软盘。我写了一个简单的输出文字“你好,世界”程序然后编译链接这个程序,总共花了8分钟。
嗯,8分钟好像太长了。我写了一个脚本程序来自动化整个过程,把时间缩减为7.5分钟。这样好一点了。但是我想设计一个可以玩奥赛罗的程序。(译者按:奥赛罗原文为Othello,一种棋类游戏,规则见http://www.ugateways.com/bof4.html)这个游戏总是能打动我。我不得不花很多时间等待编译器编译我的程序。“就是这样的,”一个专业程序员告诉我,“我们通常在办公室里房放上sit-up board(译者按:一种健身器材,可以在上面做仰卧起坐或者有氧操什么的) ,当PASCAL编译器开始运行时,我们就开始锻炼。我这样编程了几个月后,我的身材不要太棒喔!”
后来有一天,丹麦程序员写了一个很灵的叫做Compas Pascal的程序。Philippe Kahn(Borland公司的创始人)买下了它,更名为Borland Turbo Pascal。Turbo Pascal好得简直难以想象,因为它能做IBM Pascal能做的所有事情,但是只要33K内存。而且还额外提供一个编辑器。 这还不是最棒的。最棒的是编译一个小程序只需要不到一秒。这就好比一个你从来没有听说过的公司生产了通用公司别克轿车的克隆版,这种克隆车可以每小时行驶一百万英里,但是只消耗了一滴汽油。一只小小的蚂蚁喝下这点汽油也不会撑死。
突然,我的编程效率变得高的多了
那时我开始明白了“REP循环”(Rep loop)这个概念. REP是“读入,求值,打印(Read, Eval, Print)”的缩写。这个概念解释了Lisp(一种编程语言,用于人工智能)解释器的基本原理:它“读入”你的输入,计算你的输入得到结果,打印结果。下面给一个例子:我输入一些东西,Lisp解释器计算,然后输出结果。
在一个稍微大点的规模上,当你写代码时,你也处于一个REP循环的宏版本中,这个循环就是“编码-编译-测试”。你编写代码,把代码编译成可执行的文件,然后测试它,看一下运行起来怎么样。
关键一点是当你写一个程序时,你的工作过程是循环的。一个循环所花时间越短,你的生产力就越高,当然最短时间不会小于编译器运行的时间。 这就是一个程序员为什么总是要一个真正够快的硬件而编译器开发者们总是不断使他们的编译器运行更快的正式的纯计算机科学角度的原因。Visual Basic的办法是当你输入代码时,它就开始进行代码的语法解析,这样程序解释运行时速度很快。Visual C++的办法是增量编译(incremental compiles),预编译头文件(precompiled headers)和增量链接(incremental linking)。
但是一个大型的团队有多个开发人员和测试人员,你碰到了同样的循环,可能不同点就是有更多的文档要写(可是这还只是草稿,天哪!)。一个测试人员发现了bug并报告,然后开发人员修复了这个bug。那么测试人员得到修正后的代码需要多少时间?在一些软件开发机构,这样的报告-修正-再测试循环(Report-Fix-Retest loop)可能需要几个礼拜。如果一个循环需要这么长的时间,通常意味着该机构生产力很低。想让整个开发过程运转得更平滑一点,你必须想方设法使得报告-修正-再测试循环(Report-Fix-Retest loop)花的时间更少。
一个好的办法是每日构建(daily builds)。 每日构建意味着自动地,每天,完整地构建整个代码树、(译者按:“代码树”,原文为source tree,意思是将整个项目源代码的目录,子目录,文件的位置尽可能事先固定下来,这样在开发过程中各个模块间,各个文件间的相对位置都不会混乱。源代码树指的就是一个项目所有的已经组织好的代码文件。通常代码树应该用版本控制软件管理起来。虽然这个概念很基本,但是据我的观察,国内还是有软件公司在这方面做的不够好的,所以有必要解释一下。)
自动地 - 因为你设定代码每天在固定的时间构建。在Unix环境下使用cron,在windows下使用“任务计划”。
每天 - 或者更频繁. 当然每天构建的次数越多越好啦。但是有时候构建次数还是有上限的,原因和版本控制有关系,等会儿我会谈到的。
完整地 -很可能你的代码有多个版本。多语言版本,多操作系统版本,或者高端低端版本。每日构建(daily build)需要构建所有这些版本。并且每个文件都需要从头编译,而不是使用编译器的不完美的增量编译功能。
以下是每日构建(daily build)能带来的好处:
当一个bug被修正了,测试者可以很快得到最新的修正后的版本开始重新测试,以验证bug是否真正地被修复了。
开发人员可以更加确定他们对代码做的修改不会破坏1024个操作系统上的任何一个版本。验证这一点不需要在他们的机器上安装OS/2(IBM公司生产的PC机操作系统)。
那些每天将修改过的代码导入(check in)版本控制服务器的开发人员知道,他们对一个模块导入的修改不会拖别的开发人员的后腿。拖后腿的意思是,那些开发别的模块的程序员使用这个修改过的模块,出了问题,于是他们自己的模块也没有办法开发下去了。每日构建则不会有人拖后腿。如果把一个开发团队比作一台PC机,那么团队中的一个程序员对某个模块的修改导致其他人无法开发别的模块,相当于PC机发生了蓝屏。当一个程序员忘记把他(她)新建立的文件导入到repository(指版本控制服务器上的代码树)时,这种开发过程中的“蓝屏”会经常发生。因为在这个程序员自己的计算机上有这个文件,所以他(她)构建 这个程序没有问题。但是其他程序员是从版本控制服务器上导出(check out)代码的,由于服务器上没有这个文件,他们遇到了链接错误(link error),无法继续工作了。
外部团队(例如市场销售部门,进行beta测试的一些客户)可以获得一个比较稳定的版本,这样对他们开展自己的工作比较有利。
假如你将每日构建出的二进制文件(例如一个可执行程序,一个dll等等)存档管理,那么当你发现一个非常奇怪的,无法解决的bug时,你可以通过对这些文件进行二进制搜索(binary search)来确定什么时候这个bug第一次出现。如果有对代码进行了完善的版本控制,你也可以找出是谁在何时对代码进行的导入(check in)导致了这个bug。
当开发者修正了测试者报告的一个错误时,如果测试者同时报告了发现错误时的构建的版本,开发人员可以直接在那个版本中测试是否bug真正被修复了。(译者按:测试者报告出现了一个bug,只是报告了一个错误症状,而错误的原因可能有多个,每个原因可能在不同的模块中。前文中的方法是为了避免只修正了一个模块中一个原因,别的模块由于在变动,于是掩盖了而不是修复了bug)
以下是如何进行每日构建(daily build)的具体步骤。你需要用最快的电脑建立一个每日构建服务器。写一个脚本,可以自动从版本控制服务器中导出(check out)完整的代码,(你当然使用版本控制,不是吗?),然后对代码从头开始进行构建(build),要构建所有的版本。如果你有一个安装打包程序,也要在脚本中自动运行。所有会卖给最终用户的东西都要包括在构建过程中。把构建出来的版本放在各自的的目录里,不同时间构建的相同版本也应该按日期整理好,不要相互覆盖。每天固定的时间运行这样的脚本。
最关键的是所有这些步骤都应该由脚本自动化完成,从导出(check out)代码到将最终产品放在网上供用户下载(当然在开发阶段,产品是放在一台测试服务器上的)。要保证开发过程中的任何东西的任何记录是由文档记录的而不是由某个人的脑子来记录的,这是唯一的办法。否则你会碰到这样的情况,产品需要发布了,可是只有Shaniqua知道如何将产品打包的,可是他刚刚被巴士撞了。在Juno公司(本文作者工作过的公司之一),要进行每日构建,你唯一需要学会的东西就是双击每日构建服务器桌面上的一个快捷方式。
如果你在发行程序的当天发现了一个小bug,没有问题。修正它,然后重新运行每日构建脚本,现在你可以安安心心的发行程序了。当然,黄金定律是每日构建脚本应该是把所有的事情从头做一遍,遵循这个定律就不会有什么问题。
将你的编译器的警告参数设到最大(在微软的VC中是-W4 ; 在GCC中是-Wall),当遇到任何一个最微小的警告时就应该停下来。
如果每日构建失败了,可能整个开发团队的工作会因此进行不下去。当务之急是立刻找出原因,使得每日构建能成功进行下去。某天也许你会一天运行好几次每日构建脚本的。
每日构建一旦失败,应该自动地将失败用email通知整个团队。提取错误日志中的恰当部分并包括在email正文中也不是很难。每日构建脚本也可以将当前的状态报告整理成一个html文件,自动发布到一个所有人都可以访问的web服务器上,这样测试者可以很快知道那个版本的构建是成功的。
当我在微软的excel团队中工作时,我们的一个有效办法是,谁导致每日构建(daily build)失败,他(她)就得负责照看当日的每日构建(译者按:微软通常每日构建都在半夜),直到有另一个人导致构建失败而接替他(她)。这样做当然可以使每个开发者都小心不要因为自己代码的错误破坏了构建,更重要的是团队中的每个人都可以学会每日构建(daily build)的原理。
如果你的团队在同一个时区工作,在午饭时间进行每日构建(daily build)是个不错的主意。午饭前每个程序员导入(check in)代码,这样当程序员在吃饭时,构建系统在工作。当程序员吃完饭回来时,如果每日构建失败了,所有的人也都在,可以尽快找出失败的原因。当构建系统运作起来时,没有人再会担心别人导入(check in)代码会妨碍自己的工作了。.
如果你的团队同时在两个时区工作,计划好每日构建(daily build)的时间使得一个时区的工作不会影响另一个时区的工作。在Juno公司,纽约程序员在晚上7:00导入(check in)到版本控制服务器。如果他们的导入导致构建失败,印度Hyderabad市(译者按:印度科技重镇)的程序员在纽约时间晚上8:00以后的工作几乎无法进行下去。我们每天进行两次每日构建(daily build),每次构建的时间都在两地的程序员回家之前,这下就没有问题了。
更进一步的阅读:
一些关于每日构建工具的讨论
做每日构建是很重要的,它是 获得高质量代码的12个步骤之一。
Windows NT team G. Pascal Zachary关于微软Windows NT团队每周构建的书中有一些很有趣的东西。超级明星(译者按:原文为showstopper,指特别受人喜爱,杰出之人或之物).
Steve McConnell(译者按:著名的软件工程作家,Win2000开发组总负责人,其名著《Writing Solid Code》,《Debugging the Development Process》都有中文译本。) 写的关于每日构建(daily build)的文章在这里.