第2章 极限编程实践
“作为开发人员,我们要记住一点,极限编程并非唯一的选择。”
—皮特·麦克布雷恩(Pete McBreen),《软件工艺》作者
前面一章中,我们简要地概括了敏捷软件开发的内容。但是,它并没有明确地告诉我们去做些什么,除了一些说教性的陈词滥调和目标,它并没有给出实际的指导方法。本章会补充这部分内容。
极限编程实践
极限编程(eXtreme Programming,XP)是最著名的敏捷方法。它由一组简单且互相依赖的实践组成。这些实践结合在一起形成一个整体大于部分的集合。本章中,我们简要探讨一下这套实践集,在后续的章节中,我们会对其中一些实践进行单独研究。
客户团队成员
我们希望客户和开发者在一起紧密地工作,以便彼此知晓对方所面临的问题,并一起解决这些问题。
谁是客户呢?XP团队中的客户是指定义产品特性并给这些特性排列优先级的人或者团体。有时候,客户是和开发人员同属一家公司的一组业务分析师或者市场专家。有时候,客户是用户团体委派的用户代表。有时候,客户是实际支付开发费用的人。不过,在XP项目中,无论谁是客户,他们都是能够和团队一起工作的团队成员。
最好的情况是客户和开发人员在同一个房间中工作,再次一点的情况是客户和开发人员之间的距离在几十米以内。距离越大,客户就越难成为真正的团队成员。如果客户工作在另外一幢建筑或另外一个省(州),那么他就更难融入团队了。
如果确实无法和客户一起工作,该怎么办呢?我的建议是找一个愿意并能代替真正客户的人来共同工作。
用户故事
为了进行项目计划,必须要知道需求的有关内容,但无需知道得太多。若目的是做计划,只要对需求了解到足够估算的程度就够了。你可能认为,为了对需求进行估算,就必须要了解该需求的所有细节,其实并非如此,你需要知道存在很多细节,也需要知道需求的大致类别,但是不必指明特定的细节。
需求的特定细节很可能随着时间而改变,尤其是当客户看到了集成好的系统时,更会如此。看到新系统上线是关注需求的最佳时刻。因此,在离真正实现需求还很遥远的时候关注该需求的特定细节,很可能会产生浪费。
在XP中,我们会和客户反复讨论,获得对需求细节的理解,但是不去捕获那些细节。我们更愿意客户在索引卡片上写下一些我们达成共识的词语,这些只言片语可以帮助我们回忆这次交谈的内容。基本上,在客户写的同时,开发人员会在该卡片上写下对应需求的估算。估算基于我们和客户交谈过程中对细节的理解。
用户故事就是需求澄清过程中的助记词。它是一个计划工具,客户根据它的优先级和估算来安排计划。
短交付周期
XP项目每两周交付一次可工作的软件。每两周一轮迭代产出一个可以满足干系人部分需求的可工作软件。为了获得干系人的反馈,每轮迭代结束后,系统都要演示给他们看。
迭代计划
一轮迭代一般时长两周。这段期间会产生一个较小的产出物,可能会发布到生产环境。这个产出物是客户在开发者给出的预算范围内挑选出来的一组用户故事。
开发者通过测量他们在前一轮迭代中完成的用户故事给出当前迭代的预算。客户可以挑选任何数量的用户故事放入当前迭代,只要它们的估算不超出预算的范围。
一旦迭代启动,客户就承诺不会在当前迭代中改变用户故事的定义和优先级。在这段时间里,开发者可以自由地把用户故事拆分成任务(1),并且依据最符合技术和业务意义的优先级开发这些任务。
发布计划
XP团队通常会创建一次计划来规划随后大约6轮迭代的内容,这就是所谓的发布计划。一次发布通常需要3个月的工作量。它代表一次较大的交付,通常这次交付会被发布到产品环境中。发布计划由一组排好优先级的用户故事组成,这些用户故事由客户在开发者给出的预算范围内挑选而来。
开发者通过测量他们在前一个发布中完成的用户故事给出当前发布计划的预算。客户可以挑选任意数量的用户故事加入当前发布计划中,只要它们的估算不超过预算。客户也可以决定这次发布计划中需要完成的用户故事的优先级。如果团队成员强烈要求的话,客户可以指明哪些迭代应该完成哪些用户故事,据此规划出发布计划中的前几轮迭代的内容。
发布计划不是一成不变的,客户可以随时改变其中的内容。他们可以取消用户故事、编写新的用户故事或者改变用户故事的优先级。
验收测试
客户通过验收测试捕获用户故事的细节。验收测试的编写要先于或者和用户故事的实现同步进行。它们用一些脚本语言编写,这样就可以自动并重复地运行。与此同时,它们负责验证系统的行为是否符合客户的期望。
编写验收测试所使用的语言和系统的增长和演进保持同步。客户可能会招募新人开发一个简单的脚本系统,或者他们有一个独立的质量保证部门(QA)来负责开发。很多客户会借助QA来开发验收测试的工具,并且自己编写验收测试。
一旦验收测试通过,它就会被加入到通过的验收测试的集合里,并且不允许再次失败。这个逐渐增长的验收测试的集合在每次系统构建时都会运行。如果验收测试失败了,那么这次构建也会宣告失败。所以,一旦需求实现,它就永远不会被破坏。系统从一种可工作状态迁移到另一种可工作状态,绝对不允许出现超过几个小时不可工作的状态。
结对编程
所有的产品代码都应该由结对的程序员在一台开发机器上共同完成。(2)结对的两人一个掌控键盘,写代码,另一个人看着对方写,寻找错误和可以提高的地方。两个人交互频繁,全神贯注地投入编写软件的过程中。
两人频繁切换角色。掌控键盘的人可能感到疲劳或遇到困难,此时,他的同伴会接过键盘继续写。在一个小时内,键盘可能在他们之间来回传递好几次。最终的代码是由他们俩人共同设计和实现的,两人功劳均等。
结对组合至少每天要改变一次,以便每个程序员在一天内可以在两个不同的结对组合中工作。在一轮迭代过程中,每个团队成员都应该和其他团队成员结对工作过,并且所有人都应该参与本轮迭代中所涉及的每项工作。
这种做法将极大地促进知识在团队内的传播。当然,专业知识还是必不可少的,那些需要一定专业知识的任务通常需要合适的专家去完成,不过那些专家也几乎会和团队中的所有人结对。这将加快专业知识在团队内的传播。在紧要关头,团队中的其他人就能够代替专家的角色。
[Williams2000], [Cockburn2001]和[Nosek]研究成果表明,结对非但不会降低开发团队的效率,反过来还会大大降低缺陷率。
测试驱动开发
本书第4章是有关测试的内容,其中详细地讲解了测试驱动开发的方法。下面仅仅提供一个快速的预览。
所有的产品代码都是为让失败的单元测试通过而写的。首先,我们写一个失败的单元测试,因为此时它测试的功能还不存在,然后我们实现功能代码让其通过。
编写测试用例和实现代码之间的更迭速度是很快的,基本上几分钟左右。测试用例和代码共同演进,其中测试用例循序渐进地对代码的实现进行引导。
最终,一个非常完整的测试用例集就和实现代码一起发展起来了。程序员可以使用这些测试用例来检查程序的正确性。如果结对的程序员对代码做了微小改动,那么他们就可以运行测试确保没有破坏任何逻辑。这会非常有利于重构(后续章节会讨论)。
当写出的代码是想要让测试通过时,这样的代码就会被定义为可测试的代码。另外,这样做会大大激发你去解耦每个模块,以便对它们单独进行测试。因此,这样写出来的代码,设计往往是松耦合的。面向对象设计的原则在解耦方面具有巨大的促进作用。
集体所有权
结对编程中每一对成员都有权拉取(check out)和改进任何模块中的代码。没有哪个程序员单独对哪个特定的模块或技术负责。每个人都会参与GUI、中间件和数据库方面的工作。也没有人比其他人在某个模块或技术上更权威。
这并不意味着XP不需要专业知识。如果你专精于GUI领域,那么你最有可能从事GUI方面的任务,但也可能要求你去和别人结对,从事中间件和数据库方面的任务。如果你决定学习另一项专业知识,那么你可以承接相关任务,并和能够传授这方面知识的专家一起工作。你不会被限制在自己的专业领域内。
持续集成
程序员每天会多次提交(check in)代码并进行集成。规则很简单:率先提交的人成功提交到代码库,其他人得合并(merge)本地代码后才能提交。(3)
XP团队使用非阻塞的源代码控制工具(4)。这意味着程序员可以在任意时间拉取任何模块,而不管其他人是否拉取过这个模块。当程序员完成该模块的修改并提交时,必须把自己的改动和别人先于他提交的改动进行合并。为了避免合并时间过长,团队的成员会非常频繁地提交他们的模块。
结对人员会在一项任务上工作1~2个小时。他们编写测试用例和产品代码。在某个适当的间歇点,也许远远在任务完成之前,他们决定把代码提交回去。最重要的是要确保所有的测试都能通过。他们把新代码集成进代码库中。如果需要,他们会对代码进行合并。如有必要,他们还会和先于自己提交的程序员协商。一旦集成进代码仓库,他们就开始从新代码中构建出新系统(详情参见《重构》)。他们运行系统中的每一个测试,包括当前所有运行着的验收测试。如果破坏了原先可以工作的部分,他们就得进行修复。一旦所有的测试都通过,他们就算是完成了此次提交工作。
因而,XP团队每天都会进行多次系统构建,他们会重新创建整个系统。如果系统的最终结果是一个可以访问的网站,他们就部署该网站,很可能部署到一台测试服务器上。
可持续的开发速度
软件项目不是短跑比赛,而是马拉松长跑比赛。那些跃过起跑线就拼命狂奔的团队在距离终点线很远的地方就会筋疲力尽。为了快速完成开发,团队必须以一种可持续的速度前进。团队必须保持旺盛的精力和高度的警觉,必须有意识地保持稳定、适中的速度。
XP的规则是不允许团队加班的。不过,在版本发布前一周是该规则唯一的例外,如果发布目标近在眼前并且能够一蹴而就,则允许加班。
开放的工作空间
团队在一个开放的办公空间里一起工作,房间中有一些桌子,每张桌子上摆放了两三台工作机,每台工作机前有两把椅子预备给结对编程的人员,墙壁上挂满了状态图表、任务分解表和UML图等。
房间里充满了嗡嗡的交谈声。每对结对人员都坐得近,相互间可以听得到,彼此都能得知对方是否陷入麻烦,也都能了解对方的工作状态。所有人都能够随时随地参与热烈的沟通中。
可能有人觉得这种环境会分散人的注意力,很容易担心外界不断的干扰会让人什么事也做不成。但是事实并非如此。而且,密歇根大学的一项研究表明,在“作战室(war room)”里工作,生产率非但不会降低,反而会成倍提升。(http://www.sciencedaily. com/releases/2000/12/001206144705.htm。)
规划游戏
在第3章中,我会详细介绍XP中规划游戏(planning game)的内容。在这里,先简要描述一下。
计划游戏的本质是划分业务人员和开发人员之间的职责。业务人员(也就是客户)决定特性的重要性(feature指的是面向最终用户的软件所具备的功能),开发人员决定实现一个特性所花费的代价。
在每次发布和迭代的开始,开发人员会基于最近一次迭代或发布的工作量估算出当前的预算。客户挑选出的用户故事其总花销不超过预算上限。
采用这些简单的原则,经过短周期迭代和频繁的发布,客户和开发人员很快就会适应项目开发的节奏,客户在了解开发人员的速度后,就可以确定项目会持续多长时间以及会花费多少成本。
简单设计
XP团队总是尽可能把设计做得简单和富有表现力(expressive)。此外,他们仅仅关注本轮迭代中计划完成的用户故事,不会担心将来的事情。相反,他们在一次次迭代中演进系统设计,让当前系统实现的用户故事保持在最优的设计上。
这意味着XP团队不大可能从基础设施开始工作,他们不会优先选择数据库或者中间件,而是选择以尽可能简单的方式实现第一批用户故事。只有当某个用户故事迫切依赖基础设施时,才会考虑引入。
下面有三条XP咒语(mantra)可以指导开发人员。
考虑可行的最简单的事情
XP团队总是尽可能寻找针对当前用户故事的最简单的设计。在实现当前用户故事时,如果可以用平面文件(5),就不去用数据库或者EJB(企业级Java Bean);如果能用简单的套接字连接,就不去用ORB(对象请求代理)(6)或者RMI(远程方法调用)。多线程能不用就不用。我们尽量考虑用最简单的方法来实现当前的用户故事。然后,挑选一种我们能实际得到且尽可能简单的解决方案。
你并不需要它
你说得都对,但是我们知道总有一天需要数据库,总有一天需要ORB,也总有一天得去支持多用户。所以,我们现在就得为这些东西预留位置,是吧?
如果在确切需要基础设施之前拒绝引入会怎么样呢?XP团队会对此认真考虑。他们开始时假设不需要那些基础设施。只有当有证据或者至少有十分明显的迹象表明现在引入这些基础设施比继续等待更加划算时,团队才会引入基础设施。
一次且仅有一次
极限编程人员者不能容忍重复代码。无论在哪里发现重复代码,他们都会消除掉。
导致代码重复的因素有很多,最明显的是用鼠标选中一段代码后四处粘贴。当发现那些重复代码时,我们会定义一个函数或基类,用这种方法去消除。有时两个或多个算法非常相似,但是它们之间又有些微妙的差别,我们会把它们变成函数,或者运用模板方法(参见第14章的)。无论导致重复的是何种因素,只要发现,必定消除。
消除重复最好的方法就是抽象。毕竟,如果两种事物相似的话,必定可以通过某种抽象统一它们。消除重复的行为会迫使团队提炼出许多的抽象,并进一步减少代码中的耦合。
重构
第5章会对重构做详细讨论,下面只是一个简单的介绍。
代码总是会腐化。随着我们逐渐添加特性,不断处理bug,代码的结构会慢慢退化。如果置之不理,代码就会变得纠结不清,无法维护。
XP团队通过频繁地运用重构手法扭转这种局面。重构就是在不改变代码行为的前提下,进行小步改造(transformation)从而改进系统结构的实践方法。每一步改造都是微不足道的,几乎不值一提。但是所有的改造叠加到一起,就会显著地改进系统的设计和架构。
在每次小步改造后,我们运行单元测试来保证没有破坏任何功能。然后继续做下一步改造,如此往复,周而复始,每一步都要运行测试。这样,我们在改善系统设计的同时,始终保持系统可以正常运行。
重构是持续进行的,而不是在项目结束后、版本发布后、迭代结束后,甚至是每天快下班时才去做的。重构是我们每隔一个小时或者半个小时就要去做的事情。重构可以持续地让我们的代码保持尽可能干净、简单和富有表现力。
隐喻
隐喻(metaphore)是所有XP实践中最难理解的。极限编程的拥趸本质上都是务实主义者,隐喻这个缺乏具体定义的概念让我们很不舒服。的确,一些XP的倡导者经常讨论如何把隐喻从XP的实践中移除。然而,在某种意义上,隐喻却是XP中最重要的实践之一。
想象一下智力拼图玩具。你怎么知道如何把各个小块拼到一起呢?显然,每一块都和其它块相邻,并且它的形状必须与相邻的块完全吻合。假如你眼神不好但是触觉灵敏,你可以锲而不舍地筛选每个小块,不断调整位置,最终也能拼出整张图来。
不过,还有一种比摸索形状去拼图更为强大的力量,这就是整张拼图的图案。图案是真正的向导。它的力量巨大到如果图案中相邻的两块无法吻合,你就可以断定拼图玩具的制作者把玩具做错了。
这就是隐喻,它是整个系统联结在一起的全景图,它是系统的愿景,它让所有独立模块的位置和形状一目了然。如果模块的形状和整个系统的隐喻不符,那么你就可以断定这个模块是错误的。
通常,隐喻是一个名称系统,名称提供了系统元素的词汇表,它有助于定义元素之间的关系。
举个例子,我曾经做过一个系统,要求以每秒60个字符的速率把文本显示到屏幕上。在这个速率下,铺满屏幕需要花一些时间。所以,我们写了一个程序让它生成文本并填充到一个缓冲区,当缓冲区满了后,我们把程序从内存交换到磁盘上。当缓冲区见底,我们又把程序交换回内存继续运行。
我们把这个系统说成自卸卡车托运垃圾。前面的缓冲区是小型卡车,显示屏是垃圾场,我们的程序是垃圾生产者。这些名称恰如其分,也有助于我们将这个系统当成一个整体来理解。
另一个例子,我做过一个分析网络流量的系统。每隔30分钟,它就会轮询数十个网卡,从中抓取监控数据。每个网卡给我们提供一小块由几个独立变量构成的数据,我们把这些小块称为“切片”,这些切片都是原始数据需要进一步分析。分析程序需要“烹饪”这些切片,所以我们把分析程序称为“烤面包机”,把切片中的独立变量称为“面包屑”。总的来说,这个隐喻有用,也有趣。
小结
极限编程是一组构成敏捷开发流程的简单、具体实践的集合。这个流程已经运用到很多团队,也取得了不错的效果。
XP是一套优良的、通用的软件开发方法论。项目团队可以直接采用,也可以增加一些实践,或者对其中的一些实践进行修改后再采用。
参考文献
1. Dahl, Dijkstra. Structured Programming. New York: Hoare, Academic Press, 1972.
2. Conner, Dary1 R. Leading at the Edge of Chaos. Wiley, 1998.
3. Cockburn, Alistair. The Methodology Space. Humans and Technology technical report HaT TR.97.03 (dated97.10.03),http://members.aol.com/acockburn/papers/methyspace/methyspace. htm.
4. Beck, Kent. Extreme Programming Explained: Embracing Change. Reading, MA: Addison-Wesley, 1999.
5. Newkirk, James, and Robert C. Martin. Extreme Programming in Practice. Upper Saddle River, NJ: Addison-Wesley, 2001.
6. Williams, Laurie, Robert R. Kessler, Ward Cunningham, Ron Jeffries. Strengthening the Case for Pair Programming. IEEE Software, July-Aug. 2000.
7. Cockburn, Alistair, and Laurie Williams. The Costs and Benefits of Pair Programming. XP2000 Conference in Sardinia, reproduced in Extreme Programming Examined, Giancarlo 8. Succi, Michele Marchesi. Addison-Wesley, 2001.
8. Nosek, J. T. The Case for Collaborative Programming. Communications of the ACM(1998): 105-108.
9. Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Addison-Wesley, 1999.
(1)译注:任务拆分方法要符合正交且穷尽,每一个任务完成也必须是独立可验收的。
(2)我曾经见过这样结对编程的情景,一人掌控键盘,另一人控制鼠标。
(3)译注:可以参考ThoughtWorks提倡的7步提交法:1.更新代码;2.本地编码;3.本地构建;4.再次更新代码;5.本地构建;6.提交到代码仓库;7.持续集成服务器上构建。
(4)译注:事实上,现在常用的源代码控制工具都是非阻塞的,如Git、SVN和Mercurial等。
(5)译注:平面文件有别于关系型数据库,它指的是没有包含结构化索引和关系的记录文件。它可以是文本也可以是二进制文件,典型的平面文件有*nix中的/etc/passwd等。
(6)译注:对象请求代理是对象之间建立客户端/服务端关系的中间件。