购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第4章
集成:关注整体质量

集成是一个在各个行业中广泛使用的词汇。它给人的印象是:把不同的部分攒到一起,并且能够工作。在软件开发中,是不是也是这样呢?

4.1 集成的概念

集成最初的含义是,把小块的东西拼起来。如果更精确一点,那就是,不仅要拼起来,还要拼好。如果放在软件开发这个领域,那就是要让整个软件跑起来。张大侠说,他那个模块开发完了,能跑了。王大侠说,他那个模块也开发完了,能跑了。李大侠也这么说。你可不要以为万事大吉了,这几个模块放在一起,指不定啥样子呢。说不定张大侠和王大侠可能对两个模块之间的接口的理解不同,以至于这两个模块不能配合工作。也许李大侠没有考虑到王大侠的模块可能会给他传来一个超出范围的值,使得李大侠的模块在毫无征兆的情况下崩溃……

集成 (Integration)的基本使命是,把产品的各个部分捏在一起,并且保证,产品作为整体是可以运转的,而不仅是每个模块、每个单元能在特定的开发调试环境、特定的数据和参数下可以运转。

这是从一个视角看。从另一个视角看,集成是把不同的人、不同的团队的改动捏到一起,并保证系统仍能工作。集成的不是模块,而是修改变化。每个人都在这个软件产品上工作,添加、修改、删除。每个变更集,可能在一个模块上修改,也可能涉及多个模块。每个人完成一个或好几个变更集。要保证这些改动是可以累加在一起的。也就是说,新完成的若干变更集,是可以合并在一起,可以编译通过,可以运行的。从这个视角看,不再是把产品的各个模块捏到一起,而是把对产品的改变合并在一起,基于已有版本,产生新版本。所集成的,是修改变化,是变更集(见图4-1)。

图4-1 集成的含义

注意,即便是每个改动都是正确的,也不能保证捏到一起仍然是正确的。举个例子:假定有个应用程序,它的窗口的固定宽度是600像素,窗口里填充了背景图案,背景图案的宽度是200像素。也就是说,在横向上,该背景图案重复三次,挺好看。这时,因为一些原因,张大侠把应用程序的窗口的固定宽度从600像素缩小到400像素。而同时,王大侠换了张背景图案,背景图案的宽度从200像素换成150像素。这两个改动单独发生时,都没什么问题,窗口里背景图案还是齐边儿齐沿儿的。但是当这两个改动捏到一起以后,窗口里背景图案不再是齐边儿齐沿儿的了。

所以说,在软件开发领域里,集成有两个方面的含义:把各个部分捏在一起,并且保证可以工作;把各个改动捏在一起,并且保证可以工作。

注意集成有广义和狭义之分。上面是广义的集成的定义。照此定义,当开发人员在提交之前更新工作区,并作自测试的时候,他是在看他的改动是否能跟别人新近的改动一起工作。这是在做集成。当他做完他所在的模块的单元测试,又测了一下应用程序外在的行为的时候,他是在看他的模块是否能跟别的模块在一起工作。这也是在做集成。随后,系统测试团队对应用程序的一些版本做更加全面的测试,并把发现的Bug反馈给开发人员,以便修复,这个过程当然也是在做集成。等最终做好了集成,就正式发布给客户,上市卖钱。

而当人们谈论集成的时候,常常谈论的是狭义的集成,也就是集成工程师所做的工作。简单地说,集成工程师取源代码的最新版本,编译构建,并作粗略测试。如果在此过程中遇到了问题,就解决问题。都没问题了,宣布系统新的版本,供系统测试团队深入测试,也供开发人员使用。

广义的集成几乎是必需的。通过广义的集成,系统的各个部分得以配合工作,不同人的修改得以配合工作。而狭义的集成,也就是集成工程师做的集成工作,对于开发团队来说却不一定是必需的,或者说,可能很不起眼。这种情况常常发生在规模不足10人的小型团队里。在小型团队里,如果开发人员都能保证提交的质量,那么版本库里的最新版本有严重质量问题的几率就很小,开发人员可以相当有把握地随时用公共版本库里的最新版本更新自己的工作区,不必担心程序因此而转不起来。我们先从这种情况讨论起。

4.2 保证提交的质量

开发人员完成一个变更集,然后提交该变更集。在提交之前,开发人员要做一些质量保证工作。原因很简单,如果张大侠提交了很烂的代码,导致王大侠和李大侠在更新他们的工作区后,程序跑不起来了,那么王大侠和李大侠就会围殴张大侠。

当然,文明社会,围殴事件很少发生。但是显然,张大侠的不负责任的行为,对团队其他的成员们造成了伤害,耽误了他们的宝贵时间,往大里说,耽误了项目进展。为此,张大侠应做检讨,改过自新。如何改过自新呢?

在前面的章节里,我们已经提到了一些。首先,要在一个变更集完成之后再提交。干到半截的工作,可能会给团队其他成员带来困扰(详见第3.1节)。其次,要适时更新工作区,尽力避免因为跟大家不同步而带来的问题(详见第3.3节)。下面是其他一些方面。

在提交前,应该做充分的自测试。首先是单元测试(Unit Test)。尽可能保证自己所编写的那一部分是没有问题的。进而,对系统整体进行测试,保证自己所编写的那一部分,与软件的其他部分的配合是没有问题的。这些测试都可以考虑自动化。

在这个敏捷(Agile)时代,谈到单元测试,就不得不提及测试驱动开发(Test-Driven Development,TDD)。简单地讲,在这种方法中,是先写好自动测试脚本,再开发,直到测试运行通过。这样做的好处是,用简单的脚本而非繁复的文档,在开发前明确开发的目标,而在开发后又能自动地检验该目标完成的情况。

除了测试外,有的团队还会引入 同行评审 (Peer Review),又称做代码走查(Code Inspection)等。在开发人员完成一个变更集中的代码修改后,把相关的修改,送交其他开发人员(通常是1到4个)仔细阅读,找出并排除错误。同行评审应该得到工具的一定程度上的支持,让其他开发人员能够方便地看到要评审的内容:这包括,变更集中各文件修改后的版本,和各文件的改动本身,即修改前和修改后的差异对比。如果工具能够方便地记录评审意见,那就更好啦。

[Gerrit] Gerrit是一款开源的同行评审工具,与Git相集成。它“拦截”开发人员提交的变更集,只有通过了同行评审,才会“放行”。Gerrit通过网页展现变更集;评审人员可以把评审意见写到对应的代码行附近,方便直观。

与同行评审相关的是有些敏捷开发团队采用 结对编程 (Pair Programming)技术,目的之一也是提高程序质量。结对编程技术是一个非常简单和直观的概念:两位开发人员肩并肩地坐在同一台电脑前合作完成同一个设计、同一个算法、同一段代码或同一组测试。与同行评审相对比,同行评审是在变更集完成后,阅读变更集对应的代码改动;而结对编程是在变更集完成的过程中进行类似的工作。据说,与两位开发人员各自独立工作相比,结对编程会更有效率,且质量更高。要指出的是,测试驱动开发和结对编程还都是存在争议的新技术。

同行评审和结对编程是由人类来完成的。事实上,机器可以在一定程度上代替人。这被称做 静态程序分析 (Static Program Analysis)。相关的工具 能够发现一些源代码中的错误和可能的错误并报告,提醒人类注意。

最后再次强调,适时更新工作区。特别是,如果在提交之前,已经做了比较全面的测试,但已经有一段时间没更新工作区了,那么要做一次更新,然后在工作区里编译构建,并作粗略的测试,证明程序可以运行,没啥大碍。这种粗略的测试常被称做 冒烟测试 (Smoke Test)。测试之后再提交。这样就基本可以保证,提交后公共版本库里最新版本是可以工作的,不会给其他开发人员带来很大的困扰。

[Subversion] 注意,这里所说的提交前的更新,是为了质量的缘故。如果提交前,仅仅是因为Subversion工作原理而做了一次更新,那就没必要再编译链接和粗略测试了。

[Git] 同Subversion。

[ClearCase UCM] 除了更新工作区并粗略测试外,还有一个选择。粗略测试也可以在改动合并到公共流对应的工作区但还没有签入时完成。注意这是在公共流对应的工作区里完成,而非开发流对应的工作区。在确认没有问题后,再真正提交到公共流上,供其他人将来使用。

是的,只是“基本可以保证”。因为这里还有漏洞:万一在这个开发人员检测的过程中,别的开发人员提交了新的修改,而这与正在检测的修改之间有矛盾有冲突呢?如何修补这个漏洞,请看下节分解。

4.3 狭义集成的步骤

即便是大家都挺注意保证提交的质量,但是由于本质上是并行工作,因此还是有漏洞的。团队成员越多,漏洞越多,出现问题的可能性越大。能否考虑某种锁定机制来解决,把并行变成串行?这通常不是个好方法,因为可能会导致开发人员们排队提交……

要么这样吧,专门找个人,时不常地检查一下公共版本库里最新源代码的质量情况。至少是能否编译链接得过,如果过不去呢,就及时修复。如果做得更好些呢,就再粗略测试一下,如果发现有重大问题,就及时改掉。其实这就是集成工程师主要做的事情,也就是常说的(狭义的)集成工作。我们来仔细看看。

第一步,确保开发人员都提交了相关的源代码。作为集成工程师,可以提醒一下大家,本次将集成今天下午3点前提交的代码改动。也可能项目经理之类的领导在确认无误后,告诉集成工程师,没问题了,现在就做集成吧。更为严格的控制是,预先定好本次要集成的工作——这一般是一个列表。在集成开始前,检查一遍,是不是所有该做的变更集都完成并且提交了,并且,列表之外的改动有没有被提交。如果控制得再严一点,那就是,每次提交都必须先获得某种授权,有授权的才可以提交。

大多数情况下都没必要这么麻烦,但有时候确实需要严格的控制。比如,在产品即将上市,或进入维护期后,需要加强这方面的控制。以产品即将上市时为例,这个时候的首要任务是让产品趋于稳定,并且尽快上市,而非进行大幅度的结构调整或增加很多新功能等耗时费力且可能影响产品质量的事。因此,这时候的管理者要站出来,引导开发人员把精力放在当前最要紧的事情上,保证“让产品趋于稳定,并且尽快上市”这样的目标的实现。方法之一就是,在集成这个关口加强控制、加强审批。

[Subversion] 可以在工具中设置一些触发器(Trigger),并编写一些脚本,让工具在执行一些版本控制操作的时候,强制执行某些检查。

[Git] 可考虑采用拖入(Pull)的机制:不让开发人员把改动推入(Push)到公共版本库,而是集成人员从开发人员的私用版本库那里,把改动拖入到公共版本库。这样,集成人员只集成通过他检查的改动。另外也可以考虑通过推入权限的管理,或通过Gerrit。

[ClearCase UCM] 除了设置触发器外,还可以锁定公共的地盘(公共流)、仅特定用户可写,可以提交活动,而对其他人只读;或者在创建基线时,对包含的活动有所选择;或者不让开发人员提交到公共流,而是让集成人员在活动被批准时,提交执行活动;或者同时使用ClearCase和ClearQuest。ClearQuest是IBM Rational出品的变更请求管理工具,我们从第9章开始会讲到它。

第二步,冻结或者标识将要集成的源代码。这里 冻结 (Freeze)是指临时禁止开发人员继续提交代码修改,而标识通常就是打个标签之类,当然也可以记录下当时的自然版本号。为什么要做这样的操作呢?因为我们得搞清楚,集成的到底是哪些内容。等集成后,我们就可以宣称,我们已经把它们集成好啦!其中,它们是指哪些变更集,集成后形成了哪个整体版本。

冻结或者标识,都是不完美的。若采用冻结源代码的方法,会让开发人员在一段或短或长的时间里,无法提交。而若采用标识的方法,无论是打标签还是记录自然版本号,只要不冻结,那么,当需要为解决集成问题而作出一些代码修改时,有可能这些修改就会混在开发人员为了产品继续向前演进所提交的修改里,而那些修改可能是不希望出现在这次集成里的,或者那些修改中的问题又一次引起集成失败。所以说,这些方法是不完美的,但是,一般来说,这些方法已经足够好了 ,特别是标识的方法。

第三步,取出要集成的源代码 。最好是存放到一个干净的工作区。为的是清清爽爽,别无杂物。这里所说的杂物包括,一些编译的中间文件和结果文件,尚未签入或提交的本地修改,等等。这些可能会干扰接下去的工作,造成错误或混乱。当然,有些时候要做一些妥协,比如,保留编译的中间文件,以便进行增量构建,显著提高编译速度,当然这也有副作用,需要权衡。关于增量构建及其副作用,详见第5.3节。

第四步,编译、链接和打安装包。目的是由源代码转换生成可以测试、发布的安装包。这通常统称为 构建 (Build),我们会用专门的章节(第5章)来讨论它。

在构建过程中,可能会遇到问题。如果是构建环境、构建过程相关的问题,常常是由集成工程师来解决。而如果是源代码的问题,那通常就要由开发人员来负责解决了。相关开发人员跑到集成工程师这里,指导集成工程师修改一两行代码,或者坐到集成工程师的位子上,把问题解决。当然,更正规一点的方法是,让开发人员在自己的工作区里处理好,然后提交上来。

如果待解决的问题比较复杂或者比较难,预计需要比较长的时间的话,集成工程师可以采用另外一个策略,就是把有问题的提交,甚至是可能有问题的提交,从本次集成中剔出去。等开发人员弄好了,再重新提交。这样做的理由是,集成是瓶颈,很多人在等着集成的结果,很多人都在意公共版本库里代码的质量,所以集成要尽快完成,哪怕以本次集成少进一个或几个变更集为代价。

[Subversion] 执行一个反向合并操作,就可以把某个已提交的变更集剔出去。

[Git] 用反转(revert)命令。

[ClearCase] 想把已提交的内容剔出去是比较困难的。在Base ClearCase里,需要找到每个相关的文件,分别运行合并命令,跟上若干参数。在ClearCase UCM里,有个叫cset.pl的脚本能让这个事儿方便一点,但这个脚本并不是“官方”的。

如果在构建这一步遇到了问题,为此动了源代码,那么这之后,回到第一步,从头再来。

第五步,安装并粗略测试。仅仅能生成安装包,通常是不够的。总得能大致运行起来,才像那么回事儿。在这一步,像第四步一样,如果遇到问题,就要解决。如果解决的过程中动了源代码,那就回到第一步,从头再来。

为什么是粗略测试而不是详细测试呢?集成是瓶颈,很多人在等着集成的结果,所以集成要尽快完成,哪怕新版本里带几个小Bug,也就忍了吧。更详细的测试,可以等新版本出来之后再慢慢测,Bug可以随后再改。详见下一节。

第六步,标识和储存集成成果。集成成果至少有两个,一个是源代码的整体版本,一个是生成的安装包。它们都要被合理的标识和存储,供大家使用。源代码打了合适的标签或者记录下自然版本号就好啦。注意通过特定的标签命名约定或标签属性等方法,把这时候打的标签与前面第二步打的标签相区别,让大家一眼就能看出来哪些标签通过了集成,哪些(还)没有。

至于安装包,通常放到某个共享目录下存储。这个话题,在第5.5节还会详细讨论。

第七步,通知相关人员,本次集成完成。这可能包括开发人员、测试人员和各级各类领导同志。通知的方式可以是电子邮件,也可以是RSS订阅之类。除了通告集成完成之外,还应该告知集成成果的名称和存储的位置。另外,还可以在此简述本次集成对应完成了哪些开发工作、修复了哪些Bug,等等。

总之,狭义的集成,其基本的目的是产生包含了最近提交的改动的,一个有一定质量的整体版本。这个版本我们通常叫它 基线 (Baseline)。严格地说,基线既包括了对应的源代码整体版本,也包括了编译构建生成的安装包。

4.4 在基线产生之后

为什么需要集成工程师集成?为什么要产生基线?原因之一是,开发人员要用。我们以前讲过,开发人员从公共版本库中拿到最新版本,用来更新自己的工作区。其实这个说法并不严格。有些时候,开发人员是想拿到别人刚刚提交的修改,也就是公共版本库里最新的内容;而有些时候,开发人员是想拿到最新的基线,这样拿到的代码比较靠谱,没什么大问题。

事实上,即便是开发人员总是去拿公共版本库里最新的内容,集成工程师的工作对他们也是有意义的。因为做狭义集成能够发现并修复一些已提交代码中存在的严重问题,不再是每次都等到某个(或者某几个)不幸的开发人员撞见了。在统计上,即便开发人员不直接使用基线,他还是从狭义集成中受益。

另一方面,测试人员也需要使用基线。(狭义的)集成产生了具有一定质量的基线。测试人员对其中的一些基线进行全面而深入的测试。在测试过程中,不断发现问题,也就是常说的Bug。测试人员在缺陷跟踪系统里(详见第9章)记录下来这些Bug,通知开发人员整改。开发人员修复若干Bug后,集成工程师集成,产生新的基线,再次送交测试人员测试。

如此往复,直到有一天,某个领导查看了测试报告等资料后宣布,质量过关啦,拿出去卖钱吧!于是产品就对外发布啦(见图4-2)。

图4-2 开发-测试-发布

4.5 质量保证:集成前、集成中、集成后

发现没有,广义的集成其实发生在三个阶段中。第一个阶段,狭义集成之前。其实就是开发人员的改动提交到公共版本库之前。在这个阶段,开发人员更新工作区、代码评审、编译链接、做一系列的自测试,等等。这些都是为了发现和改正代码中的问题,包括集成问题。

第二个阶段,狭义集成过程中。集成工程师收集最近提交的代码改动,在一起编译链接,并粗略测试,然后发布基线给大家。这些工作的主要目的也是发现和改正代码中的问题,其中主要是集成问题。

第三个阶段,狭义集成后。测试人员使用基线进行系统全面的测试,发现问题,并交由开发人员改正。

这三个阶段,都在做质量保证工作,都在消灭集成过程中的问题,直到最后,达到一定的质量标准,集成完成。注意三个阶段的质量保证工作,有它们各自的特点:

第一阶段,狭义集成前

在第一阶段里,不论是代码评审还是自测试,质量保证工作主要围绕着发生的代码改动及其周围进行,进行深入的检查,并改正问题。

这样的工作可以有效率地发现问题,因为问题通常出现在代码改动及其周围,一抓一个准儿。

这样的工作也可以有效率地解决问题,不必把集成工程师或测试人员折腾进来,也不必在缺陷跟踪工具里走一系列流程。集成工程师或测试人员来掺乎的话,他(们)得去判断该去找谁,再找到他,再沟通交流,等完成了,再验收,有问题,再去找,等等。如果开发人员在变更集提交之前就弄好,那这些事儿就都没了。

如果一些重要问题没有在此发现,留到第二阶段狭义集成的时候暴露出来,会增加集成工程师的负担,而他的工作是串行地解决出现的问题,一个问题一个问题地分析并解决。不像各个开发人员在提交前是并行地发现和解决问题:每个开发人员随时都在解决,通常互相之间不用等待、排队。因此,集成工程师的工作,容易成为瓶颈。即便没有形成瓶颈,他耗费的时间,通常比某个开发人员耗费的时间更能影响项目的进展,因为大家都在等着集成工程师的工作成果,也就是基线。因此,要尽量避免重要问题到狭义集成的时候才暴露出来。

而如果一些在此阶段发现和改正的成本并不高的问题留到了第三阶段才去发现和改正的话,除了发现和改正的成本可能升高外,还有可能影响到了开发人员的开发,或影响到了测试人员的测试。开发人员会被不相干的问题所打扰。比如,想给图片显示软件加个随机播放功能,可是即便是顺序播放时,软件都会不稳定,因此开发随机播放功能遇到问题时,开发人员就会困惑:是我的代码有问题还是别人的代码早就有问题?测试人员也可能被影响。假定某个游戏有5个场景,顺序出现。如果第2个场景有个Bug,程序总是到这儿就退出了,那么剩下的3个场景就没法测了,只能等这个Bug修复了才能继续测。

第二阶段,狭义集成中

在第二阶段里,质量保证工作主要是在有限的时间里,保证程序在整体上没什么大问题。

加上这一道检查,能够防止更多的人被同样的重大问题影响。这些重大问题,比如编译不通过,会对开发人员有广泛的、严重的影响,应该尽量避免。

还要注意,此阶段的检查不宜用太长的时间。太长的时间,就意味着等待新的基线需要很长的时间,延误项目。相关的,此阶段的质量标准,也不宜设得太高。太高的质量标准,基线产生过程中就会遇到太多的问题必须要解决,这使得基线的产生更为困难,需要更多的时间。

第三阶段,狭义集成后

在第三阶段里,质量保证工作是对程序进行全面的、广泛的、细致的检查,找出问题,改正问题。

为什么不在第一阶段进行“全面的、广泛的、细致的检查”?因为没效率。在第三阶段的某次“全面的、广泛的、细致的检查”发现了来自各个开发人员提交的、积累下来的20个Bug,但如果每个开发人员都在第一阶段进行一次“全面的、广泛的、细致的检查”,那可能只能比“围绕着发生的代码改动及其周围”进行的检查多发现一两个Bug。而一次“全面的、广泛的、细致的检查”可能成本很高,不是用人时来衡量,而是要用人月来衡量。

如果硬要开发人员这样没效率地工作,开发人员就会有一种自然的倾向,攒上很多代码改动,一起测试,一股脑提交。于是,开发人员之间因为任务相互依赖而互相等待的情况就增加了,项目关键路径延长了。并且,开发人员分头开发,前进很久才合并,会导致更多的合并问题……因为这些违背了持续集成(详见第6章)这一基本原则。

总之,各个阶段的质量保证工作既不能缺,也不能走过头,要恰到好处。

集成之道,中庸之道。 nFa6XLJSiIJarywcXBYC229Jg0kzWVGlnXIZYD1xT7bx2ljCxSjQhiawGSeCrRpJ

点击中间区域
呼出菜单
上一章
目录
下一章
×