第2章 极限编程概述
作为开发人员,我们应该记住,XP并非唯一选择。--Pete McBreen,软件技术专家
在第1章中,我们概述了有关敏捷软件开发方法方面的内容,但它没有确切地告诉我们去做些什么;其中给出了一些泛泛的陈述和目标,却没有给出实际的指导方法。本章要改变这种状况。
2.1 极限编程实践
2.1.1 完整*队
我们希望客户、管理者和开发人员紧密地工作在一起,以便于彼此知晓对方所面临的问题,并共同去解决这些问题。谁是客户?XP*队中的客户是指定义产品的特性并排列这些特性优先级的人或者*体。有时,客户是和开发人员同属一家公司的一组业务分析师、质量保证专家和/或者市场专家。有时,客户是用户*体委*的用户代表。有时,客户事实上是支付开发费用的人。但是在XP项目中,无论谁是客户,他们都是能够和*队一起工作的*队成员。
最好的情况是客户和开发人员在同一个房间中工作,次一点的情况是客户和开发人员之间的工作距离在100m以内。距离越大,客户就越难成为真正的*队成员。如果客户工作在另外一幢建筑或另外一个州,那么他将会很难融合到*队中来。
如果确实无法和客户工作在一起,该怎么办呢?我的建议是去寻找能够在一起工作、愿意并能够代替真正客户的人。
2.1.2 用户故事
为了进行项目计划,必须要了解需求,但是却无需了解得太多。对于做计划而言,了解需求只需要到能够估算它的程度就足够了。你可能认为,为了对需求进行估算,就必须要了解该需求的所有细节。其实并非如此。你必须知道存在很多的细节,也必须知道细节的大致分类,但是你不必知道特定的细节。
需求的具体细节很可能会随时间而改变,一旦客户开始看到集成到一起的系统,就更会如此。看到新系统的问世是关注需求的最好时刻。因此,不要去捕获某个在很长一段时间之后才会实现的需求的特定细节,否则很可能会导致无用功以及对需求不成熟的关注。
在XP中,我们和客户反复讨论,以获取对于需求细节的理解,但是不去记录那些细节。我们更愿意客户在索引卡片上写下一些共识的言语,这些只言片语可以提醒我们记起这次交谈。基本上在客户进行书写的同一时刻,开发人员在该卡片上写下对应于卡片上需求的估算。估算是基于在和客户进行交谈期间所得到的对于细节的理解进行的。
用户故事(user story)就是正在进行的关于需求的谈话的助记符。它是一个计划工具,客户可以使用它并根据需求的优先级和估算代价来安排实现该需求的时间。
2.1.3 短交付周期
XP项目每两周交付一次可以工作的软件。每两周的迭代都实现了利益相关者的一些需求。在每次迭代结束时,会给利益相关者演示迭代生成的系统,以得到他们的反馈。
迭代计划
每次迭代通常耗时两周。迭代是一次较小的交付,可能会被加入到产品中,也可能不会。迭代计划由一组用户故事组成,这些用户故事是客户根据开发人员确定的预算选出来的。
开发人员通过度量在以前的迭代中所完成的工作量来为本次迭代设定预算。只要估算成本的总量不超过预算,客户就可以为本次迭代选择任意数量的用户故事。
一旦迭代开始,客户就同意不再修改当次迭代中用户故事的定义和优先级别。迭代期间,开发人员可以自由地将用户故事分解成任务(task),并依据最具技术和商业意义的顺序来开发这些任务。
发布计划
XP*队通常会创建一个发布计划来规划出随后大约6次迭代的内容。这就是所谓的发布计划。一次发布通常需要3个月的工作。它表示了一次较大的交付,通常此次交付会被加入到产品中。发布计划是由客户根据开发人员给出的预算所选择的、排好优先级别的一组用户故事组成。
开发人员通过度量在以前的发布中所完成的工作量来为本次发布设定预算。只要估算成本的总量不超过预算,客户就可以为本次发布选择任意数目的用户故事。客户同样可以决定在本次发布中用户故事的实现顺序。如果开发*队强烈要求的话,客户可以通过指明哪些用户故事应该在哪次迭代中完成的方式,制订出发布中最初几次迭代的内容。
发布计划不是一成不变的。客户可以随时改变发布的内容。他可以取消用户故事,编写新的用户故事,或者改变用户故事的优先级别。但是,客户应该尽量不去更改一次迭代。
2.1.4 验收测试
可以以客户指定的验收测试的形式来记录有关用户故事的细节。用户故事的验收测试是在就要实现该用户故事之前,或者在实现该用户故事的同时才开始编写的。验收测试使用脚本语言编写,这样它们可以自动、反复地运行 。这些测试共同来验证系统是否按照客户指定的行为运转。
验收测试是由业务分析师、质量保证专家以及测试人员在迭代期间编写的。编写验收测试使用的语言对于程序员、客户以及业务人员来说都很容易阅读和理解。程序员就是从这些测试中了解他们正在实现的故事的真实工作细节。这些测试成为真正的项目需求文档。验收测试描述了每个特性的所有细节,并用作验证这些特性是否被正确完成的决定性依据。
一旦通过一项验收测试,就将该测试加入到已经通过的验收测试集合中,并决不允许该测试再次失败。这个不断增长的验收测试集合每天会多次运行,每当系统被创建时,都要运行这个验收测试集。如果一项验收测试失败了,那么系统创建就宣告失败。因而,一项需求一旦被实现,就再不会遭到破坏。系统从一种工作状态迁移到另一种工作状态,期间,系统的不能工作状态时间决不允许超过几个小时。
2.1.5 结对编程
代码都是由结对的程序员使用同一台工作站共同完成的。结对人员中,一个控制键盘并输入代码。另一个观察着输入的代码,寻找着代码中的错误和可以改进的地方 。两个人认真地进行着交互。他们都全身心地投入到软件的编写中。
两人频繁互换角色。控制键盘的可能累了或者遇到了困难,他的同伴会取得键盘的控制权。在一个小时内,键盘可能在他们之间来回传递好几次。最终生成的代码是由他们两人共同设计、共同编写的,两人功劳均等。
结对的关系要经常变换。每天至少要改变一次,这样每个程序员在一天中可以在两个不同的结对中工作。在一次迭代期间,每个*队成员应该和所有其他的*队成员在一起工作过,并且他们应该参与了本次迭代中所涉及的每项工作。
结对编程会极大地促进知识在*队中的传播。仍然会需要一些专业知识,那些需要一定专业知识的任务通常需要合适的专家去完成,但是那些专家几乎将会和*队中的所有其他人结对。这将加快专业知识在*队中的传播。这样,在紧要关头,其他*队成员就能够代替所需要的专家。Williams 和Nosek 的研究表明,结对非但不会降低编程人员的效率,反而会大大减少缺陷率。
2.1.6 测试驱动开发
第4章会详细地讨论这个主题。在此,我们仅进行大致的介绍。
编写所有产品代码的目的都是为了使失败的单元测试能够通过。首先编写一个单元测试,由于它要测试的功能还不存在,所以它会运行失败。然后,编写代码使测试通过。
编写测试用例和代码之间的更迭速度是很快的,基本上在几分钟左右。测试用例和代码共同演化,其中测试用例循序渐进地对代码的编写进行指导(参见第6章中的例子)。
作为结果,一个非常完整的测试用例集就和代码一起发展起来。程序员可以使用这些测试来检查程序是否正确地工作。如果结对的程序员对代码进行了小的更改,那么他们可以运行测试,以确保更改没有对程序造成任何的破坏。这会非常有利于重构(在本章后面介绍)。
当为了使测试用例通过而编写代码时,那么所编写的代码天生就是可测试的。更重要的是,这样做会强烈地激发你去解除各个模块间的耦合,以便能够独立地对它们进行测试。因而,以这种方式编写的代码的设计往往具有更弱的耦合。面向对象设计的原则在进行这种解耦方面具有巨大的帮助作用(参见本书第二部分)。
2.1.7 集体所有
每一对编程者都具有签出(check out)任何模块并对它进行改进的权力。每个程序员都不会对任何一个特定的模块或技术单独负责。每个人都参与GUI方面的工作 ,每个人都参与中间件方面的工作,每个人都参与数据库方面的工作。任何人都不会比其他人在一个模块或者技术上具有更多的权威。
这并不意味着XP不需要专业知识。如果你的专业领域是有关GUI的,那么你最有可能去从事GUI方面的任务,但是你也将会被邀请去和别人结对从事有关中间件和数据库方面的任务。如果你决定去学习另一门专业知识,那么你可以承担相关的任务,并和能够传授你这方面知识的专家一起工作。你不会被限制在自己的专业领域。
2.1.8 持续集成
程序员每天会多次签入(check in)他们的代码并进行集成。规则很简单:第一个签入的只要完成签入就可以了,所有后面签入的人负责代码的合并工作。
XP*队使用非阻塞的源代码控制工具。这就意味着程序员可以在任何时候签出任何模块,而不管是否有其他人已经签出了这个模块。当程序员完成了对于模块的修改并把该模块签入时,他必须把他所做的改动和在他前面签入该模块的程序员所作的任何改动进行合并。为了避免合并的时间过长,*队的成员会非常频繁地检查他们的模块。
结对人员会在一项任务上工作一到两个小时。他们创建测试用例和产品代码。在某个适当的间歇点,也许远在这项任务完成之前,他们决定把代码签入回去。他们首先确保所有的测试都能够通过,然后把新的代码集成进当前的代码库中。如果需要,他们会对代码进行合并。如果有必要,他们会和在签入上有冲突的其他程序员协商。一旦集成进了他们的更改,他们就构建新的系统。他们运行系统中的每一个测试,包括当前所有有效的验收测试。如果他们破坏了原先可以工作的部分,他们会进行修正。一旦所有的测试都通过了,他们就算完成了此次签入工作。
因而,XP*队每天会进行多次系统构建。他们会从头开始创建整个系统 。如果系统的最终结果是一张CD,他们就刻录该CD。如果系统的最终结果是一个可以访问的Web站点,他们就安装该Web站点,或许会把它安装在一个测试服务器上。
2.1.9 可持续的开发速度
软件项目不是全速短跑,它是马拉松长跑。那些一跃过起跑线就开始尽力狂奔的*队将会在远离终点前就筋疲力尽。为了快速地完成开发,*队必须要以一种可持续的速度前进。*队必须保持旺盛的精力和敏锐的警觉。*队必须要有意识地保持稳定、适中的速度。
XP的规则不允许*队加班工作。在版本发布前的一个星期是该规则的唯一例外。如果发布目标就在眼前并且能够一蹴而就,则允许加班。
2.1.10 开放的工作空间
*队在一个开放的房间中一起工作。房间中有一些桌子。每张桌子上摆放了两到三台工作站。每台工作站前有两把椅子。墙壁上挂满了状态图表、任务明细表、UML图,等等。
房间里充满了交谈的嗡嗡声,结对编程的两人坐在互相能够听得到的距离内,每个人都可以得知另一人是否遇到了麻烦,每个人都了解对方的工作状态,程序员们都处在适合于激烈地进行讨论的位置上。
可能有人认为这种环境会分散人的注意力。很容易会让人担心由于持续的噪音和干扰而一事无成。事实上并非如此。而且,密歇根大学的一项研究表明,在"充满积极讨论的屋子"(war room)里工作,生产率非但不会降低,反而会成倍地提高 。
2.1.11 计划游戏
第3章中会详细介绍XP的计划游戏。在这里,仅做简要介绍。
计划游戏(planning game)的本质是划分业务和开发之间的职责。业务人员(也就是客户)决定特性的重要性,开发人员决定实现一个特性所花费的代价。
在每次发布和迭代的开始,开发人员向客户提供一个预算。客户选择那些所需的代价合计起来小于等于该预算的用户故事。开发者所提供的预算是基于他们在最近一次迭代或者发布中所完成的工作量进行的。
依据这些简单的规则,采用短周期迭代和频繁的发布,很快客户和开发人员就会适应项目的开发节奏。客户会了解开发人员的开发速度。基于这种了解,客户能够确定项目会持续多长时间,以及会花费多少成本。
2.1.12 简单设计
XP*队使他们的设计尽可能的简单、有表达力。此外,他们仅仅关注于计划在本次迭代中要完成的用户故事,而不会考虑那些未来的用户故事。*队更愿意在一次次的迭代中不断地变迁系统的设计,使之对正在实现的用户故事而言始终保持在最优状态。
这意味着XP*队的工作可能不会从基础设施开始。他们并不先去选择数据库或者中间件,而是先以最简单的可能方式实现第一批用户故事。只有当出现一个用户故事迫切需要基础设施时,他们才会引入该基础设施。
下面3条XP指导原则(mantra)可以对开发人员进行指导。
(1) 考虑能够工作的最简单的事情。XP*队总是尽可能寻找能实现当前用户故事的最简单的设计。在实现当前的用户故事时,如果能够使用平面文件,就不去使用数据库;如果能够使用简单的socket连接,就不去使用ORB或者Web Service;如果能够不使用多线程,就别去用它。我们尽量考虑用最简单的方法来实现当前的用户故事。然后,选择一种我们能够实际得到的和该简单性最接近的解决方案。
(2) 你不需要它。是的,但是我们知道总有一天会需要数据库,会需要ORB,也总有一天得去支持多用户。所以,我们现在就需要为那些东西做好准备,不是吗?
如果在确实需要基础设施前拒绝引入它,那么会发生什么呢?XP*队会对此进行认真的考虑。他们开始时假设将不需要那些基础设施。只有在有证据,或者至少有十分明显的迹像表明现在引入这些基础设施比继续等待更加合算时,*队才会引入这些基础设施。
(3) 一次,并且只有一次。极限编程者不能容忍重复的代码。无论在哪里发现重复的代码,他们都会消除这些重复。
导致代码重复的因素有许多。最明显的是通过鼠标选中一段代码,然后四处进行粘贴。当发现那些重复的代码时,我们会通过创建一个函数或基类的方法来消除它们。有时两个或多个算法非常相似,但是它们之间又存在有微妙的差别,我们会把它们变成函数,或者使用TEMPLATE METHOD模式(请参见第22章)。无论重复代码源于何处,一旦发现,就必须被消除。
消除重复最好的方法就是抽象。毕竟,如果两种事物相似的话,必定存在某种抽象能够统一它们。这样,消除重复的行为会迫使*队提炼出许多的抽象,并进一步减少代码间的耦合。
2.1.13 重构
第5章会对重构进行详细的讨论 ,下面只是一个简单的介绍。
代码往往会腐化。随着我们添加一个又一个的特性,处理一个又一个的错误,代码的结构会逐渐退化。如果对此置之不理的话,这种退化最终会导致纠结不清、难于维护的混乱代码。
XP*队通过经常性的代码重构来扭转这种退化。重构就是在不改变代码行为的前提下,对其进行一系列小的改造,旨在改进系统结构的实践活动。每个改造都是微不足道的,几乎不值得去做。但是所有的这些改造叠加在一起,就形成了对系统设计和构架显著的改进。
在每次细微改造之后,我们都会运行单元测试以确保改造没有造成任何破坏,然后再去做下一次改造。如此往复,周而复始。通过这种方式,我们可以在改造系统设计的同时,保持系统可以工作。
重构是持续进行的,而不是在项目结束时、发布版本时、迭代结束时甚至每天快下班时才进行的。重构是我们每隔一个小时或者半个小时就要去做的事情。通过重构,我们可以持续地保持代码尽可能干净、简单并且具有表达力。
2.1.14 隐喻
隐喻(metaphor)是唯一一个不具体、不直接的XP实践,也是所有XP实践中最难理解的一个。极限编程者在本质上都是务实主义者,隐喻这个缺乏具体定义的概念使我们觉得很不舒服。的确,一些XP的支持者经常讨论把隐喻从XP的实践中去除。然而,在某种意义上,隐喻却是XP所有实践中最重要的实践之一。
想象一下智力拼图玩具。你怎样知道如何把各个小块拼在一起?显然,每一块都与其他块相邻,并且它的形状必须与相邻的块完美地吻合。如果你眼睛看不见但是具有很好的触觉,那么通过锲而不舍地筛选每个小块,不断地尝试它们的位置,也能够拼出整个图形。
但是,相对于各个小块的形状而言,还有一种更为强大的力量把这些复杂的小块拼装在一起。这就是整张拼图的图案。图案是真正的向导。它的力量是如此之大,以至于如果图案中相邻的两块不具有互相吻合的形状,那么你就可以断定拼图玩具的制作者把玩具做错了。
这就是隐喻。它是将整个系统联系在一起的全局视图。它是系统的愿景,是它使得所有单独模块的位置和外观变得明显直观。如果模块的外观与整个系统的隐喻不符,那么你就知道该模块是错误的。
隐喻通常可以归结为一个名字系统。这些名字提供了一个系统组成元素的词汇表,并且有助于定义它们之间关系。
例如,我曾经开发过一个以每秒60个字符的速度将文本输出到屏幕的系统。以这样的速度,字符充满整个屏幕需要一段时间。所以我们让产生文本的程序把产生的文本放到一个缓冲区中。当缓冲区满了的时候,我们把该程序交换到磁盘上。当缓冲区快要变空时,我们把该程序交换回来并让它继续运行。
我们用装卸卡车拖运垃圾来比喻整个系统。缓冲区是小卡车。屏幕是垃圾场。程序是垃圾制造者。所有的名字相互吻合,这有助于我们从整体上去考虑系统。
举另一个例子,我曾经开发过一个分析网络流量的系统。每30分钟,系统会轮询几十个网络适配器,并从中获取监控数据。每个网络适配器为我们提供一小块由几个单独变量组成的数据。我们称这些数据块为"面包切片"。这些面包切片是待分析的原始数据。分析程序"烤制"这些切片,因而被称为"烤面包机"。我们把数据块中的单个变量称为"面包屑"。总之,它是一个有用并且有趣的隐喻。
当然,隐喻不仅仅是一个名字系统。隐喻是系统的愿景,它指导着所有开发者去选择合适的名字,把函数放到合适的位置,创建出新的合适的类和方法,等等。
2.2 结论
极限编程是一组简单、具体的实践,这些实践结合在一起形成了一个敏捷开发过程。极限编程是一种优良、通用的软件开发方法。对于大多数项目*队来说,可以拿来直接采用,也可以增加一些实践,或者对其中的一些实践进行修改后再采用。