2.5 案例分析(第 2 天)中的问题点

    让我们一起来反思一下第二天中发生的事情。

    2.5.1 问题 5 :不运行系统就无法察觉问题

    这个例子中到底发生了些什么呢?

    前一天几个人分头修正了 4 个 bug。但第二天早上全员的修正代码合并到一起测试时,4 个 bug 中只有 3 个被修正了,并且过去的 bug 还发生了退化。如果开发人员没有说谎的话,那么就有可能是修正的内容互相发生了干扰。

    在这个例子中,版本管理系统使用方法不正确是造成他人的修改内容被覆盖并消失的主要原因。仔细地跟踪调查版本管理系统中的提交记录,就能够彻底地弄清原因。

    但这里的问题是:到发现问题时,已经过去了一天一夜。等到发现过去修正的 bug 的退化,那经过的时间就更长了。

    假如因为几个月前的修正而发生了退化,那么追查提交记录并查明原因大概需要多久的时间呢?请试着想象一下。这时你可能连想都不愿意想,更别提去做了。难道就没有办法早一点发现这个问题吗?

    每次向版本管理系统提交更新时,都对程序整体是否能正常 build、已有的功能是否正常运行进行检查不就可以了吗?

    这样的想法称为持续集成(CI)。这是将团队成员的修改等所有项目相关的资源集中到一起进行集成,并经常、持续地确认 build 及测试是否通过的一种实践。

    CI 相关的内容将在第 5 章中进行讲解,用 CI 实现高效的回归测试相关的内容将在第 7 章中进行讲解。

    2.5.2 问题 6 :覆盖了其他组员修正的代码

    多人开发程序时会发生因修改的代码重叠而产生冲突的现象。发生冲突的情况下,要在保证双方修改都能正常运行的前提下进行合并,但要正确地进行合并是非常困难的。并且无法保证每个团队成员都能 进行合并,偷偷地直接将他人的修改覆盖而不进行合并也完全有可能不被发现。

    Subversion 和 Git 等比较新的版本管理系统的设计思想是:原则上不对文件加锁,并对多人的修改进行合并处理,有冲突时会明示并让冲突发生。

    另一方面,以前的 VSS(Visual Source Safe)19 等版本管理系统的设计思想则是对文件加锁来避免冲突的发生。

    19 微软在 2012 年之前提供支持的版本管理系统。现在提供的是其后续产品 Team Foundation Server。

    如果团队中有工程师是在使用 VSS 等基于锁的版本管理系统的开发现场成长起来的话,可能会因为不习惯 Subversion 和 Git 这类基于合并的管理系统的思维方式,而对于发生冲突时一定要消除冲突无法理解。也可能是因为开发人员觉得无视冲突,强行覆盖原有代码不会有什么问题,结果造成了类似这种现象的发生。

    关于使用版本管理系统来消除冲突的方法,以及基于合并的版本管理系统更为优秀的原因等,将在第 3 章中进行讲解。

    在类似于这次例子的情况中,如果写好测试用例并用 CI 进行测试的话,应该就能及早发现问题。CI 相关的内容将在第 5 章进行讲解。

    2.5.3 问题 7 :无法自信地进行代码重构

    几处修改 bug 的代码因被其他组员的提交所覆盖而消失了。通过追查提交记录终于把问题搞清楚了,但怎么修改才是最优雅的,成了件烦心的事情。

    在多人进行的开发中,修改的地方发生冲突是因为在同一处地方基于不同的目的添加了不同的代码。这意味着需要以某种形式来重新考虑代码的构造,也就是说需要进行重构。

    根据维基百科,重构的定义是这样的。

    重构(refactoring)是指计算机编程中,在不影响输出结果的前提下对代码内部的构造进行整理。

    正如上面所描述的“不影响输出结果”,重构必须保证程序的对外输出保持不变。也就是说需要定义出该程序中什么是正确的。

    方法之一就是准备好规格说明等资料。这个方法的确可以证明“正确性”,但每次都要手动确认重构后程序的动作是否符合规格要求,实在太耗费时间了。

    一想到又费时又麻烦,心理上对于重构的抵触情绪就愈发高涨,不愿意动手去做。在一些开发现场,“不要动已经在运行的程序”像这样明令禁止重构的情况也是存在的。

    为了消除这样的抵触情绪,能够自信地进行重构,测试代码的编写就显得尤为重要。如前所述,重构是“在不影响输出结果的前提下对代码内部的构造进行整理”,因此只要编写的测试代码可以保证输出结果不发生变化就可以了 20 。将测试代码做成只调用一个命令就能执行的形式,这样就可以简单地反复进行测试,从而在任何时间都可以迅速地对程序的正确性进行确认。只有有了这样的测试环境,才能重新着手重构工作。

    20 最好是对对象类中的 public 方法,即公开的 API 编写单元测试。但是由于类的分割不合理、数据库或用户界面和系统的耦合过于紧密等原因,无法编写单元测试的情况也时有发生。在这样的情况下,可以对系统最外侧的 API(一般情况下是用户界面)编写测试代码。相关内容将在第 7 章中进行讲解。

    能够为编写测试代码提供方便的测试框架有很多。测试框架以及测试代码的写法将在第 5 章进行讲解。

    ◆◆◆

    成功编写测试代码后,心理上对于重构的抵触情绪就能大幅减少。在进行代码重构后并提交到版本管理系统的代码库之前,调用一条命令执行测试,这样就能对重构内容的正确性进行确认。所以即使重构中发生了错误,也能在提交之前及时发现。

    进一步导入 CI,让测试代码能够一直自动执行。对程序正确性进行测试的机会越多,越能够安心地进行重构。即使在本地环境中通过测试,和其他开发人员修改的代码合并后仍有可能测试失败。并且开发人员毕竟也是人,所以在提交之前忘记执行测试也是有可能的。导入 CI 的话,因为 CI 服务器会自动执行测试,所以就能够及时发现问题。CI 服务器可以每天定时执行测试,也可以每当向版本管理系统的代码库进行提交时执行测试,根据配置可以在各种时间点执行测试并发现问题。通过编写测试代码以及导入 CI,终于可以自信地进行代码重构了,产品的品质也自然而然地有了提高。

    相反,既不写测试代码,也不进行 CI,并且也不进行重构,这样持续维护的代码就会成为巨大的负担,直至阻碍事业的发展。这样的代码在《修改代码的艺术》21 中被定义为 Legacy code。

    21 《修改代码的艺术》(美)Michael C. Feathers 著,候伯薇译,机械工业出版社 2014 年出版。——译者注

    不编写测试代码导致产生大量的 legacy code,因此软件的品质完全无法提高。确认“正确性”的手段只有手动和用眼睛看,在这样的情况下,“正确性”的确认就会白白浪费大量时间,执行回归测试 22 就更不现实了。越没时间越不写测试代码,从而产生越来越多的 legacy code,这样便陷入了恶性循环之中。

    22 该测试的目的是检查程序的修改所带来的影响。具体请参考第 7 章。

    这样的恶性循环持续几年后便会陷入绝境,不要说添加新功能了,连 bug 修正都忙不过来,最终只能被时代所淘汰。这样的软件产品并不在少数。

    关于重构所必需的测试代码的写法,以及持续地自动进行测试的 CI 实践,这些将在第 5 章中进行讲解。

    2.5.4 问题 8 :不知道 bug 的修正日期,也不能追踪退化

    在刚才的例子中,发生了以前修正的 bug 再度出现(发生了退化)的情况。不知道 bug 是什么时候发生的、是怎样的 bug、在哪次提交中被修正,这样的事情在已经运营较长时间的系统中可能是比较常见的。如果仅用邮件或口头交流故障和 bug,没有在团队成员之间共享信息,就容易发生这样的事情。为此,首先使用缺陷管理系统将问题从发生到解决的所有过程记录下来是非常重要的。关于缺陷管理系统,将在第 4 章进行讲解。

    然后,通过使版本管理系统和缺陷管理系统进行交互,就能关联代码的修改记录和问题票,并记录下来。这样就可以从问题票追踪到代码的修改记录,找出 bug 是何时修正的、谁修正的、如何修正的这些信息。反过来也可以从版本管理系统上的修改记录追踪到描述问题的 bug 票。如此一来,就既可以从问题票追踪代码,查看代码被做了怎样的修改,即过去的问题票的处理结果,又可以从代码的提交记录追溯问题票,查看问题的原因,使双向追踪成为了可能。

    版本管理系统和缺陷管理系统高效进行交互的方法将在第 4 章中进行讲解。只需稍微花一些功夫,问题的可追踪性 23 就会有所提升,这是非常有效的实践。

    23 追踪的可能性的意思。

    并且,CI 和缺陷管理系统以及版本管理系统这三者之间的交互也是非常重要的。这样一来,某个问题是在什么时候被什么人怎样修改的,以及修改结果是否通过了测试、是否反映到了 staging 环境、是否发布到了正式环境等,整个过程就都可以进行追踪。CI 和缺陷管理系统以及版本管理系统的交互,可以毫不夸张地说是现代系统开发中的三种神器。特别是在实行敏捷开发的情况下,这些是最基础的实践项目。关于这部分将在第 5 章中进行讲解。

    更进一步,如果和部署自动化工具相关联,那么到部署、发布为止就都可以进行统一的管理。近年来,一些最先进的开发现场已经在尝试自动化部署。包括自动化部署在内的管理相关的内容将在第 6 章进行讲解。

    2.5.5 问题 9 :没有灵活使用分支和标签

    这里举的是修正结束后回到新功能开发时,差点忘记合并的例子。问题 3 中已经提到过,这是因为没有合理使用版本管理系统的分支和标签功能而产生的问题。关于使用版本管理系统有效地并行开发多个任务的方法将在第 3 章中进行讲解。

    2.5.6 问题 10 :在测试环境、正式环境上无法运行

    这应该是开发现场常有的事,以“在自己的本地环境上能正常运行”为由,而无视 staging 环境或正式环境中发生的问题,这样的开发人员有时还是会遇到的。和这样的开发人员是无法进行沟通的。而将在开发环境中运行的内容在测试环境中运行起来要费一番功夫,将在 staging 环境中运行的内容在正式环境中运行起来也要费一番功夫,这样的话题倒也经常听说。

    根据环境的不同,程序运行的动作发生变化的问题通常称为“环境依赖问题”。由于环境依赖而产生的问题究竟是怎样的呢?以下是一些常见的情况:

    • 由于数据库模式的差异而产生的问题

    • 没有安装程序所依赖的库而产生的问题

    • httpd 或 memcached 等各种中间件由于环境不同而配置发生变化的问题

    数据库模式差异的管理问题,已经在问题 4 中讨论过,具体将第 3 章中进行讲解。程序依赖库的问题也将在第 3 章的依赖关系的管理这一小节中进行讲解。

    中间件等配置的问题,必然会因为环境的不同而产生差异,管理起来的确是比较困难的。在第 3 章和第 6 章中将介绍管理相关的一些小技巧。

    为了实现高效、高品质的开发,这些问题不能用“环境依赖”一语带过,而应该试着去摸索解决方案。作为全世界开发人员努力的结果,现在已经出现了帮助解决这类问题的工具。这部分内容将在第 6 章中进行讲解。

    2.5.7 问题 11 :发布太复杂,以至于需要发布手册

    这个问题和问题 10 也是相通的。无论哪里的现场,向正式环境进行发布都是件复杂并且伴有紧张感的事情。数百行的发布手册由多人确认两三遍,即使如此谨慎地进行发布,大多数情况下也都不会很顺利。经历了各种困难终于让程序运行了起来,如果能不出故障、持续地运行的话,那就是上天保佑了。这难道不是开发现场的实际情况吗?

    并且在大多数情况下,大家往往会忘记将只对正式环境进行的作业提交到代码库,或者忘记反映到发布手册上。如果还不得不去解决其他故障的话,那就更不用说了。再加上还要提交故障说明报告、向发布手册添加数量庞大的确认项目,或者确认这些项目的人数大大增加,这样一来就会使业务更加复杂。

    另一方面,多数热门的 Web 服务都以惊人的势头一边修正 bug 一边开发着新功能。例如社交编程服务,也就是 Git 的托管服务——大名鼎 鼎的 GitHub24 在 1 天之内要进行 100 次以上的发布 25 。

    24 https://github.com/

    25 Deploying at GitHub(https://github.com/blog/1241-deploying-at-github

    怎样才能做到以这样惊人的速度一个接一个地进行发布呢?至少可以知道肯定不是用发布手册人工介入进行的。

    就算 GitHub 这样惊人的发布频率属于例外情况,但如果部署和发布能够通过自动化简化一些的话,不仅能够减少作业中的错误,包括用户和开发人员在内,大家都会对此喜闻乐见吧。

    为了实现上述内容,需要解决环境依赖的问题,还需要实现自动化测试和自动化部署。换言之就是持续地维持“随时都能发布”的状态是非常重要的。

    这样的想法称为持续交付(CD)。CD 是有一定难度的实践,但一旦实现,团队的敏捷开发效率就会有飞跃般的提升。这部分内容将在第 6 章进行讲解。