《敏捷软件开发:原则、模式与实践(C#版.修订版)》—第2章2.1节极限编程实践

本节书摘来自异步社区《敏捷软件开发:原则、模式与实践(C#版.修订版)》一书中的第2章2.1节极限编程实践,作者【美】Robert C. Martin , Micah Martin,更多章节内容可以访问云栖社区“异步社区”公众号查看。

第二部分 敏捷设计
敏捷软件开发:原则、模式与实践(C#版.修订版)
如果敏捷是指以微小增量的方式构建软件,那么究竟如何设计软件呢?又如何保证软件具有灵活、可维护以及可重用的良好结构呢?如果以微小增量的方式构建软件,难道不是先制造许多无用的代码碎片,然后再打着重构的旗号返工吗?难道不会让人只见树木,不见森林吗?

在敏捷团队中,愿景和软件一起演化。在每次迭代中,团队改进系统设计,使设计尽可能适合于当前系统。团队不会花费许多时间去预测未来的需求和需要,也不会试图在今天就构建一些基础设施去支撑那些他们认为明天才会需要的特性。他们更愿意关注当前的系统结构,并使它尽可能地好。

这种做法并不是要放弃构架或者设计,而是一种增量地演化出系统最佳构架和设计的方式。同样也是一种保持设计和构架一直适合于不断增长和演化的系统的方式。在敏捷开发中,设计和构架的过程是持续不断的。

我们如何知道软件设计的优劣呢?第7章中列举并描述了拙劣设计的症状。这些症状,或者说设计臭味,常常遍及整个软件结构。第7章演示了那些症状如何在一个软件项目中累积,并描述了如何避免它们。

这些症状定义如下:

僵化性(rigidity)——设计难以改变。
脆弱性(fragility)——设计易于遭到破坏。
顽固性(immobility)——设计难以重用。
粘滞性(viscosity)——难以做正确的事情。
不必要的复杂性(needless complexity)——过分设计。
不必要的重复(needless repetition)——滥用鼠标进行复制、粘贴。
晦涩性(opacity)——混乱的表达。
这些症状在本质上和代码的“臭味”(smell)相似,但是它们所处的层次稍高一些。它们是遍及整个软件结构的臭味,而不仅仅是一小段代码的。

设计中的臭味是一种症状,是可以主观(如果不能客观的话)进行度量的。这些臭味常常是由于违反了一个或者多个设计原则而导致的。第8章~第12章中描述了一些面向对象设计的原则,这些原则有助于开发人员消除拙劣设计的症状(设计臭味),并帮助他们构建出最适合于当前特性集的设计。

这些原则如下。

第8章:单一职责原则(The Single Responsibility Principle,SRP);
第9章:开放—封闭原则(The Open-Close Principle,OCP);
第10章:Liskov替换原则(The Liskov Substitution Principle,LSP);
第11章:依赖倒置原则(The Dependency-Inversion Principle,DIP);
第12章:接口隔离原则(The Interface Segregation Principle,ISP)。
这些原则是数十年软件工程经验来之不易的成果。它们不是某一个人的成果,而是许许多多软件开发人员和研究人员思想和著作的结晶。虽然在此把它们表述为面向对象设计的原则,但是事实上它们只是软件工程中一直存在的原则的特例而已。

敏捷团队应用这些原则只是为了除去臭味。当没有臭味时,他们不会应用这些原则。仅仅因为是一个原则就无条件地遵循它是错误的。这些原则只是为了帮助我们去除一些坏味道,而不是可以随意在系统中到处喷洒的香水。过分遵循这些原则会导致不必要的复杂性的设计臭味。

2.1 极限编程实践
敏捷软件开发:原则、模式与实践(C#版.修订版)
2.1.1 完整团队
我们希望客户、管理者和开发人员紧密地工作在一起,以便于彼此知晓对方所面临的问题,并共同去解决这些问题。谁是客户?XP团队中的客户是指定义产品的特性并排列这些特性优先级的人或者团体。有时,客户是和开发人员同属一家公司的一组业务分析师、质量保证专家和/或者市场专家。有时,客户是用户团体委派的用户代表。有时,客户是真正支付开发费用的人。但是在XP项目中,无论谁是客户,他们都是能够和团队一起工作的团队成员。

最好的情况是客户和开发人员在同一个房间中工作,次一点的情况是客户和开发人员之间的工作距离在100m以内。距离越大,客户就越难成为真正的团队成员。如果客户工作在另外一幢建筑或另外一个城市,那么他将会很难融合到团队中来。

如果确实无法和客户工作在一起,该怎么办呢?我的建议是去寻找能够在一起工作、愿意并能够代替真正客户的人。

2.1.2 用户故事
为了进行项目计划,必须要了解需求,但是却无需了解得太多。对于做计划而言,了解需求只需要到能够估算它的程度就足够了。你可能认为,为了对需求进行估算,就必须要了解该需求的所有细节。其实并非如此。你必须知道存在很多的细节,也必须知道细节的大致分类,但是你不必知道特定的细节。

需求的具体细节很可能会随时间而改变,一旦客户开始看到集成到一起的系统,就更会如此。看到新系统的问世是关注需求的最好时刻。因此,不要去捕获某个在很长一段时间之后才会实现的需求的特定细节,否则很可能会导致无用功以及对需求不成熟的关注。

在XP中,我们和客户反复讨论,以获取对于需求细节的理解,但是不去记录那些细节。我们更愿意客户在索引卡片上写下一些共识的言语,这些只言片语可以提醒我们记起这次交谈。基本上在客户进行书写的同一时刻,开发人员在该卡片上写下对应于卡片上需求的估算。估算是基于在和客户进行交谈期间所得到的对于细节的理解进行的。

用户故事(user story)就是正在进行的关于需求的谈话的助记符。它是一个计划工具,客户可以使用它并根据需求的优先级和估算代价来安排实现该需求的时间。

2.1.3 短交付周期
XP项目每两周交付一次可以工作的软件。每两周的迭代都实现了利益相关者的一些需求。在每次迭代结束时,会给利益相关者演示迭代生成的系统,以得到他们的反馈。

迭代计划
每次迭代通常耗时两周。迭代是一次较小的交付,可能会加入到产品中,也可能不会。迭代计划由一组用户故事组成,这些用户故事是客户根据开发人员确定的预算选出来的。

开发人员通过度量在以前的迭代中所完成的工作量来为本次迭代设定预算。只要估算成本的总量不超过预算,客户就可以为本次迭代选择任意数量的用户故事。

一旦迭代开始,客户就同意不再修改当次迭代中用户故事的定义和优先级别。迭代期间,开发人员可以*地将用户故事分解成任务(task),并依据最具技术和商业意义的顺序来开发这些任务。

发布计划
XP团队通常会创建一个发布计划来规划出随后大约6次迭代的内容。这就是所谓的发布计划。一次发布通常需要3个月的工作。它表示了一次较大的交付,通常此次交付会被加入到产品中。发布计划是由客户根据开发人员给出的预算所选择的、排好优先级别的一组用户故事组成。

开发人员通过度量在以前的发布中所完成的工作量来为本次发布设定预算。只要估算成本的总量不超过预算,客户就可以为本次发布选择任意数目的用户故事。客户同样可以决定在本次发布中用户故事的实现顺序。如果开发团队强烈要求的话,客户可以通过指明哪些用户故事应该在哪次迭代中完成的方式,制订出发布中最初几次迭代的内容。

发布计划不是一成不变的。客户可以随时改变发布的内容。他可以取消用户故事,编写新的用户故事,或者改变用户故事的优先级别。但是,客户应该尽量不去更改一次迭代。

2.1.4 验收测试
可以以客户指定的验收测试的形式来记录有关用户故事的细节。用户故事的验收测试是在就要实现该用户故事之前,或者在实现该用户故事的同时才开始编写的。验收测试使用脚本语言编写,这样它们可以自动、反复地运行1。这些测试共同来验证系统是否按照客户指定的行为运转。

验收测试是由业务分析师、质量保证专家以及测试人员在迭代期间编写的。编写验收测试使用的语言对于程序员、客户以及业务人员来说都很容易阅读和理解。程序员就是从这些测试中了解他们正在实现的故事的真实工作细节。这些测试成为真正的项目需求文档。验收测试描述了每个特性的所有细节,并用作验证这些特性是否被正确完成的决定性依据。

一旦通过一项验收测试,就将该测试加入到已经通过的验收测试集合中,并决不允许该测试再次失败。这个不断增长的验收测试集合每天会多次运行,每当系统被创建时,都要运行这个验收测试集。如果一项验收测试失败了,那么系统创建就宣告失败。因而,一项需求一旦被实现,就再不会遭到破坏。系统从一种工作状态迁移到另一种工作状态,期间,系统的不能工作状态时间决不允许超过几个小时。

2.1.5 结对编程
代码都是由结对的程序员使用同一台工作站共同完成的。结对人员中,一个控制键盘并输入代码。另一个观察着输入的代码,寻找着代码中的错误和可以改进的地方2。两个人认真地进行着交互。他们都全身心地投入到软件的编写中。

两人频繁互换角色。控制键盘的可能累了或者遇到了困难,他的同伴会取得键盘的控制权。在一个小时内,键盘可能在他们之间来回传递好几次。最终生成的代码是由他们两人共同设计、共同编写的,两人功劳均等。

结对的关系要经常变换。每天至少要改变一次,这样每个程序员在一天中可以在两个不同的结对中工作。在一次迭代期间,每个团队成员应该和所有其他的团队成员在一起工作过,并且他们应该参与了本次迭代中所涉及的每项工作。

结对编程会极大地促进知识在团队中的传播。仍然会需要一些专业知识,那些需要一定专业知识的任务通常需要合适的专家去完成,但是那些专家几乎将会和团队中的所有其他人结对。这将加快专业知识在团队中的传播。这样,在紧要关头,其他团队成员就能够代替所需要的专家。Williams3和Nosek4的研究表明,结对非但不会降低编程人员的效率,反而会大大减少缺陷率。

2.1.6 测试驱动开发
第4章会详细地讨论这个主题。在此,我们仅进行大致的介绍。

编写所有产品代码的目的都是为了使失败的单元测试能够通过。首先编写一个单元测试,由于它要测试的功能还不存在,所以它会运行失败。然后,编写代码使测试通过。

编写测试用例和代码之间的更迭速度是很快的,基本上在几分钟左右。测试用例和代码共同演化,其中测试用例循序渐进地对代码的编写进行指导(参见第6章中的例子)。

作为结果,一个非常完整的测试用例集就和代码一起发展起来。程序员可以使用这些测试来检查程序是否正确地工作。如果结对的程序员对代码进行了小的更改,那么他们可以运行测试,以确保更改没有对程序造成任何的破坏。这会非常有利于重构(在本章后面介绍)。

当为了使测试用例通过而编写代码时,那么所编写的代码天生就是可测试的。更重要的是,这样做会强烈地激发你去解除各个模块间的耦合,以便能够独立地对它们进行测试。因而,以这种方式编写的代码的设计往往具有更弱的耦合。面向对象设计的原则在进行这种解耦方面具有巨大的帮助作用(参见本书第二部分)。

2.1.7 集体所有
每一对编程者都具有签出(check out)任何模块并对它进行改进的权力。每个程序员都不会对任何一个特定的模块或技术单独负责。每个人都参与GUI方面的工作5,每个人都参与中间件方面的工作,每个人都参与数据库方面的工作。任何人都不会比其他人在一个模块或者技术上具有更多的权威。

这并不意味着XP不需要专业知识。如果你的专业领域是有关GUI的,那么你最有可能去从事GUI方面的任务,但是你也将会被邀请去和别人结对从事有关中间件和数据库方面的任务。如果你决定去学习另一门专业知识,那么你可以承担相关的任务,并和能够传授你这方面知识的专家一起工作。你不会被限制在自己的专业领域。

2.1.8 持续集成
程序员每天会多次签入(check in)他们的代码并进行集成。规则很简单:第一个签入的只要完成签入就可以了,所有后面签入的人负责代码的合并工作。

XP团队使用非阻塞的源代码控制工具。这就意味着程序员可以在任何时候签出任何模块,而不管是否有其他人已经签出了这个模块。当程序员完成了对于模块的修改并把该模块签入时,他必须把他所做的改动和在他前面签入该模块的程序员所作的任何改动进行合并。为了避免合并的时间过长,团队的成员会非常频繁地检查他们的模块。

结对人员会在一项任务上工作一到两个小时。他们创建测试用例和产品代码。在某个适当的间歇点,也许远在这项任务完成之前,他们决定把代码签入回去。他们首先确保所有的测试都能够通过,然后把新的代码集成进当前的代码库中。如果需要,他们会对代码进行合并。如果有必要,他们会和在签入上有冲突的其他程序员协商。一旦集成进了他们的更改,他们就构建新的系统。他们运行系统中的每一个测试,包括当前所有有效的验收测试。如果他们破坏了原先可以工作的部分,他们会进行修正。一旦所有的测试都通过了,他们就算完成了此次签入工作。

因而,XP团队每天会进行多次系统构建。他们会从头开始创建整个系统6。如果系统的最终结果是一张CD,他们就刻录该CD。如果系统的最终结果是一个可以访问的Web站点,他们就安装该Web站点,或许会把它安装在一个测试服务器上。

2.1.9 可持续的开发速度
软件项目不是全速短跑,它是马拉松长跑。那些一跃过起跑线就开始尽力狂奔的团队将会在远离终点前就筋疲力尽。为了快速地完成开发,团队必须要以一种可持续的速度前进。团队必须保持旺盛的精力和敏锐的警觉。团队必须要有意识地保持稳定、适中的速度。

XP的规则不允许团队加班工作。在版本发布前的一个星期是该规则的唯一例外。如果发布目标就在眼前并且能够一蹴而就,则允许加班。

2.1.10 开放的工作空间
团队在一个开放的房间中一起工作。房间中有一些桌子。每张桌子上摆放了两到三台工作站。每台工作站前有两把椅子。墙壁上挂满了状态图表、任务明细表、UML图,等等。

房间里充满了交谈的嗡嗡声,结对编程的两人坐在互相能够听得到的距离内,每个人都可以得知另一人是否遇到了麻烦,每个人都了解对方的工作状态,程序员们都处在适合于激烈地进行讨论的位置上。

可能有人认为这种环境会分散人的注意力。很容易会让人担心由于持续的噪音和干扰而一事无成。事实上并非如此。而且,密歇根大学的一项研究表明,在“充满积极讨论的屋子”(war room)里工作,生产率非但不会降低,反而会成倍地提高7。

2.1.11 计划游戏
第3章中会详细介绍XP的计划游戏。在这里,仅做简要介绍。

19

计划游戏(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章会对重构进行详细的讨论8,下面只是一个简单的介绍。

代码往往会腐化。随着我们添加一个又一个的特性,处理一个又一个的错误,代码的结构会逐渐退化。如果对此置之不理的话,这种退化最终会导致纠结不清、难于维护的混乱代码。

XP团队通过经常性的代码重构来扭转这种退化。重构就是在不改变代码行为的前提下,对其进行一系列小的改造,旨在改进系统结构的实践活动。每个改造都是微不足道的,几乎不值得去做。但是所有的这些改造叠加在一起,就形成了对系统设计和构架显著的改进。

在每次细微改造之后,我们都会运行单元测试以确保改造没有造成任何破坏,然后再去做下一次改造。如此往复,周而复始。通过这种方式,我们可以在改造系统设计的同时,保持系统可以工作。

重构是持续进行的,而不是在项目结束时、发布版本时、迭代结束时甚至每天快下班时才进行的。重构是我们每隔一个小时或者半个小时就要去做的事情。通过重构,我们可以持续地保持代码尽可能干净、简单并且具有表达力。

2.1.14 隐喻
隐喻(metaphor)是唯一一个不具体、不直接的XP实践,也是所有XP实践中最难理解的一个。极限编程者在本质上都是务实主义者,隐喻这个缺乏具体定义的概念使我们觉得很不舒服。的确,一些XP的支持者经常讨论把隐喻从XP的实践中去除。然而,在某种意义上,隐喻却是XP所有实践中最重要的实践之一。

想象一下智力拼图玩具。你怎样知道如何把各个小块拼在一起?显然,每一块都与其他块相邻,并且它的形状必须与相邻的块完美地吻合。如果你眼睛看不见但是具有很好的触觉,那么通过锲而不舍地筛选每个小块,不断地尝试它们的位置,也能够拼出整个图形。

但是,相对于各个小块的形状而言,还有一种更为强大的力量把这些复杂的小块拼装在一起。这就是整张拼图的图案。图案是真正的向导。它的力量是如此之大,以至于如果图案中相邻的两块不具有互相吻合的形状,那么你就可以断定拼图玩具的制作者把玩具做错了。

这就是隐喻。它是将整个系统联系在一起的全局视图。它是系统的愿景,是它使得所有单独模块的位置和外观变得明显直观。如果模块的外观与整个系统的隐喻不符,那么你就知道该模块是错误的。

隐喻通常可以归结为一个名字系统。这些名字提供了一个系统组成元素的词汇表,并且有助于定义它们之间的关系。

例如,我曾经开发过一个以每秒60个字符的速度将文本输出到屏幕的系统。以这样的速度,字符充满整个屏幕需要一段时间。所以我们让产生文本的程序把产生的文本放到一个缓冲区中。当缓冲区满了的时候,我们把该程序交换到磁盘上。当缓冲区快要变空时,我们把该程序交换回来并让它继续运行。

我们用装卸卡车拖运垃圾来比喻整个系统。缓冲区是小卡车。屏幕是垃圾场。程序是垃圾制造者。所有的名字相互吻合,这有助于我们从整体上去考虑系统。

举另一个例子,我曾经开发过一个分析网络流量的系统。每30分钟,系统会轮询几十个网络适配器,并从中获取监控数据。每个网络适配器为我们提供一小块由几个单独变量组成的数据。我们称这些数据块为“面包切片”。这些面包切片是待分析的原始数据。分析程序“烤制”这些切片,因而被称为“烤面包机”。我们把数据块中的单个变量称为“面包屑”。总之,它是一个有用并且有趣的隐喻。

当然,隐喻不仅仅是一个名字系统。隐喻是系统的愿景,它指导着所有开发者去选择合适的名字,把函数放到合适的位置,创建出新的合适的类和方法,等等。

1参见www.fitnesse.org。
2我曾经见过这样的结对编程的情景,其中一个成员控制键盘,另一个成员控制鼠标。
3[Williams2000], [Cockburn2001]。
4[Nosek98]。
5这里我不是在提倡3层构架。我只是选择了软件技术的3个常见部分。
6 Ron Jeffries讲到,“End to end is farther than you think.”
7[Fowler99]。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

上一篇:Java集合源码学习(四)HashMap分析


下一篇:阿里云服务器使用初体验