敏捷软件开发
作为三篇系列文章的第一篇,我们将带你了解敏捷软件开发的重要做法——如何使用它们、你可能会碰到什么样的问题,以及你将从它们那里获得什么。
敏捷软件开发不是一个具体的过程,而是一个涵盖性术语(umbrella term),用于概括具有类似基础的方式和方法。这些方法,其中包括极限编程(Extreme Programming)、动态系统开发方法(Dynamic System Development Method)、SCRUM、Crystal和Lean等,都着眼于快速交付高质量的工作软件,并做到客户满意。
尽管构成这个敏捷开发过程的每种方法都具有类似的目标,但是它们实现这个目标的做法(practice)却不尽相同。我们把在自己完成所有过程中经历过的最佳做法集中到了本系列的文章里。
下面的图表基本勾画出了我们提炼出来的这些敏捷开发最佳做法。最中间的圆环代表一对程序员日常工作的做法。紧接着的中间一个圆环表示开发人员小组使用的做法。最外面的一个圆环是项目所涉及的所有人的做法——客户、开发人员、测试人员、业务分析师等等。
这些圆环里的所有做法都直接与四个角上显示的敏捷开发的核心价值相关:沟通(Communication)、反馈(Feedback)、勇气(Courage)和简单(Simplicity)。也就是说,每个做法都给予我们一条实现敏捷开发价值并让它们成为该过程一部分的具体方法。
在理想状况下,如果决定采用敏捷软件开发的方法,你就应该在一个经过管理层许可的敏捷开发实验项目里尝试所有的作法。这是掌握敏捷开发的最好方法之一,因为这样能保证得到支持,为你的努力提供更多的回报,帮助捕捉学习到的东西,这样你才能让敏捷开发过程来适应你独特的环境。
然而,这并不总是可行的,所以有的时候最好采用步步为营的方法。在这种情况下,我们建议从最里面的圆环向外面的圆环推进。也就是从开发人员实践开始,然后是小组这一层次的做法,最后再融入“统一小组(one team)”的概念。
为技术优势设个限——开发人员做法
技术优势是敏捷开发过程的核心。为了让其他的做法真正生效,我们必须在开发人员中进行技术优势的培训。从表面上看,技术优势可能看起来并不是核心优先对象,但是如果把我们注意力都放在上面,它将确保我们编写出不同寻常的优秀代码。这反过来同样会给予公司、客户,以及用户对软件和对我们交付能力的信心。
开发人员做法(developer practice)是我们推动技术优势的切实可行的方法。即使是独立完成,而没有其他敏捷开发做法的介入,开发人员做法也能够给你的软件带来巨大的收益。
开发人员做法可以被分解为四个做法(如果你把实际的编写代码的过程加上去就是五个做法)。它们分别是测试-编码-重整循环(Test-Code-Refactor cycle)、配对编程(Pair Programming)和简单设计(Simple Design)等。
测试-编码-重整(TCR)循环——第一步
由测试驱动的开发和重整常常被当作是各自独立做法,但是它们事实上是TCR循环的一部分。要建立我们正在寻求的紧密反馈循环,我们就需要把它们放在一起。
我们在这里的目标有两层:测试让我们对代码质量的充满信心,并能表明我们加入新代码的时候没有破坏任何东西;重整和测试有助于让代码变成我们就代码实际在做什么而进行沟通的最真实形式——任何人都应该可以看到它,并知道什么是什么。
由测试驱动的开发(TDD)是一个循环,它从测试失败开始,然后是编写足够的代码通过测试,再是重整代码,使得代码在实现系统当前功能的条件下尽可能地简单。
测试-编码-重整循环非常短暂——也就几分钟。如果超出这个时间范围那就意味着测试的级别过高,有可能加入了未经测试的实现代码。
在本文的开始部分,我们不会举出TDD的例子,有关的内容会在后面2, 3, 4详细讨论。在这里,从整体上把握并把重点放在TCR循环更有趣的方面上会更加有用。
就同任何极限编程/敏捷开发项目一样,要做的第一个素材(story)是一个经过简化的应用程序,用来完整地说明程序的功能。在本文里,这样的应用程序是一个二十一点纸牌游戏。在经过简化的第一个素材里,只有一个玩家外加一个发牌人,每个玩家只会得到两张牌,获胜者是两张牌发完后点数最大的人。
素材/要求
一个简单的二十一点纸牌游戏
· 玩家下注
· 给玩家和发牌人每人两张牌
· 给获胜者支付奖金(玩家获胜的机会为2:1)
验收测试
要知道我们的素材什么时候完成就需要经过一系列验收测试。我们这个简单游戏的验收测试如下:
玩家获胜 |
发牌人获胜 |
平局 |
玩家赌注总额=100 |
玩家赌注总额=100 |
玩家赌注总额=100 |
发牌人赌注总额=1000 |
发牌人赌注总额=1000 |
发牌人赌注总额=1000 |
玩家下注10 |
玩家下注10 |
玩家下注10 |
玩家发到10 & 9 |
玩家发到8 & 9 |
玩家发到8 & 9 |
发牌人发到8 & 9 |
发牌人发到10 & 9 |
发牌人发到8 & 9 |
玩家赌注总额=110 |
玩家赌注总额=90 |
玩家赌注总额=100 |
发牌人赌注总额=990 |
发牌人赌注总额=1010 |
发牌人赌注总额=1000 |
任务
素材往往单独解决起来往往非常困难,所以在一般情况下我们都把它分解为一系列任务来完成。在本文的二十一点纸牌游戏里,需要进行下列任务:
· 创建一副牌
· 创建一个投注台面
· 创建一手牌
· 创建游戏
· 创建一副牌
在把素材分解成为任务的时候,我们可以把各个任务再分解成一系列待办事项,从而指导我们进行测试。这让我们可以保证在通过所有测试之后完成这个任务。对于这一副牌,我们有下列事项需要完成。
· 向牌桌上放一张纸牌
· 在发牌的同时将其从牌桌上移走
· 检查牌桌是否为空
· 检查牌桌上纸牌的张数
· 将牌桌上的一副牌的张数限制为52张(如果超过,就要显示异常)
· 不断发牌,直到发完
· 洗牌
· 检查牌桌上纸牌的张数是否正确
在进行过第一轮的几个简单测试之后,我们的待办事项列表就像下面这样了:
· 向牌桌上放一张纸牌
· 在发牌的同时将其从牌桌上移走
· 检查牌桌是否为空
· 检查牌桌上纸牌的张数
· 将牌桌上一副牌的张数限制为52张(如果超过,就要显示异常)
· 不断发牌,直到发完
· 洗牌
· 检查牌桌上纸牌的张数是否正确
下一个要进行的测试是从牌桌上发牌。当我们在为测试方法编写代码的时候,我们所扮演的角色就是将要编写的应用程序的用户。这就是为什么我们给自己的类创建的接口要与给用户的接口像类似的原因。在本文的这个例子里,我们将按照命令/查询分离原则(Command/Query Separation Principle5)编写出下面这样的代码。
Deck类。如列表A所示。
列表A
import java.util.List; import java.util.ArrayList; public class Deck { private static final int CARDS_IN_DECK = 52; private List cards = new ArrayList(); public boolean isEmpty() { return size() == 0; } public int size() { return cards.size(); } public void add(int card) throws IllegalStateException { if(CARDS_IN_DECK == size()) throw new IllegalStateException("Cannot add more than 52 cards"); cards.add(new Integer(card)); } public int top() { return ((Integer) cards.get(0)).intValue(); } public void remove() { cards.remove(0); } }
我们所有的测试都通过了,而且我们没有看到任何重复或者其他必要的重整,所以应该是时候进行下面的测试了。然而事实却不是这样的。我们top和remove方法的实现里有一个潜在的问题。如果对一个空的Deck调用它们,会发生什么?这两个方法都会从纸牌的内部列表里跳出一个IndexOutOfBoundsException异常,但是目前我们还没有就这个问题进行沟通。回头看看简单性的原则,我们知道自己需要沟通。我们的类的用户应该知道这个潜在的问题。幸运的是,我们将这种测试当作是一种沟通的方式,因此我们增加了下面的测试。
public void testTopOnEmptyDeck() { Deck deck = new Deck(); try { deck.top(); fail("IllegalStateException not thrown"); } catch(IllegalStateException e) { assertEquals("Cannot call top on an empty deck", e.getMessage()); } } public void testRemoveOnEmptyDeck() { Deck deck = new Deck(); try { deck.remove(); fail("IllegalStateException not thrown"); } catch(IllegalStateException e) { assertEquals("Cannot call remove on an empty deck", e.getMessage()); } }
上面都是异常测试(Exception Test2)的例子。我们再一次运行这些测试看它们失败,然后加入实现让它们通过。
public int top() { if(isEmpty()) throw new IllegalStateException("Cannot call top on an empty deck"); return ((Integer) cards.get(0)).intValue(); } public void remove() { if(isEmpty()) throw new IllegalStateException("Cannot call remove on an empty deck"); cards.remove(0); }
尽管guard语句有重复,但是我们决定不去管它,没有将它们简化成一个共同的方法。这是因为沟通的价值超过了重复的代价,当然这只是一个个人的选择。
一手牌
我们已经完成了对牌桌和投注台面的测试和实现,现在就到了创建一手牌的时候了。待办事项列表再一次发挥其作用,我们得到了下面这样一个列表:
· 创建一个一开始没有纸牌的空手
· 向手上加入纸牌
· 检查一只手是否击败了另一手
· 检查一只手是否爆了
为空手增加一个测试很简单,我们继续到给手上加入纸牌。
public void testAddACard()
{
Hand hand = new Hand();
hand.add(10);
assertEquals(1, hand.size());
hand.add(5);
assertEquals(2, hand.size());
}
我们运行测试,然后加入实现。
public void add( int card )
{
cards.add(new Integer(card));
}
测试通过了,我们没有看到Hand类里有任何重复。但是我们刚刚给Hand加上的实现和给Deck加上的方法极其相似。回头看看牌桌的待办事项列表,我们记得必须检查牌桌(上纸牌的张数)是否正确,我们最后也对手做同样的事情。
public void testAddInvalidCard() { Hand hand = new Hand(); try { hand.add(1); fail("IllegalArgumentException not thrown"); } catch(IllegalArgumentException e) { assertEquals("Not a valid card value 1", e.getMessage()); } try { hand.add(12); fail("IllegalArgumentException not thrown"); } catch(IllegalArgumentException e) { assertEquals("Not a valid card value 12", e.getMessage()); } }
我们加入了下面的实现来通过测试。
public void add( int card )
{
if(card < 2 || card > 11)
throw new IllegalArgumentException("Not a valid card value " + card);
cards.add(new Integer(card));
}
但是现在我们在Deck和Hand里有相同的guard语句,用来检查该自变量是否代表着正确的纸牌值。简单性的原则要求我们删除重复,但是在这里情况并不像Extract Method重整6这么简单。如果我们看到多个类之间存在重复,这意味着我们缺失了某种概念。在这里我们很容易就看到Card类担负起了判断什么值是有效的责任,而Deck和Hand作为Card的容器变得更具沟通性。
我们引入了Card类以及相应的Deck和Hand重整,如列表B:
public class Card { private final int value; public Card( int value ) { if( value < 2 || value > 11 ) throw new IllegalArgumentException( "Not a valid card value " + value ); this.value = value; } public int getValue() { return value; } } public class Deck { private static final int[] NUMBER_IN_DECK = new int[] {0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 16, 4}; … public void add( Card card ) throws IllegalStateException { if(NUMBER_IN_DECK[card.getValue()] == countOf(card)) throw new IllegalStateException("Cannot add more cards of value " + card.getValue()); cards.add(card); } public Card top() { if(isEmpty()) throw new IllegalStateException("Cannot call top on an empty deck"); return (Card) cards.get(0); } … private int countOf(Card card) { int result = 0; for(Iterator i = cards.iterator(); i.hasNext(); ) { Card each = (Card) i.next(); if(each.getValue() == card.getValue()) result++; } return result; } } public class Hand { … public void add( Card card ) { cards.add(card); } … }
测试-编码-重整循环的每一阶段都涉及不同类型的思想。在测试阶段,重点放在了被实现的类的接口上。编写代码是为了让测试尽可能快地通过测试。而重整阶段可以被当作是使用简单性原则进行指导的微型代码审查。有没有重复的或者看起来类似的代码,不仅仅是在当前的类里,而且是在系统的其他类里?现在的实现可能会出现什么问题,类的用户能够与之顺利沟通吗?
重要的成功因素
· 小步前进——TCR对于开发人员来说不是一个很容易的转换。一次只进行一个步骤,同时还要明白它学习起来有一定难度。
· 严格遵守原则——只进行TDD或者只进行重整并不能让整个TCR循环一蹴而就。给自己足够的时间来尝试,并取得效果。压力和最终期限会迫使小组回到原来的习惯上——一定要小心!
· 重整过程——与小组的所有成员交换意见,了解一下他们的反馈
· 理解——确保整个小组都完全理解TCR循环是什么,如何实现它。考虑一下就此主题进行员工培训和讲座。
配对编程——第二步
TCR循环可以由某个开发人员独自完成,但是敏捷开发和TCR循环的真正威力来自于配对编程(pair programming)。在敏捷开发里,开发人员每两人一组编写所有的生产代码,其中一人担当“驱动者(driver)”(负责操作鼠标和键盘),而另一个人同驱动者一道解决问题和规划更大的图景。编程配对里的这个驱动者可以按需要进行轮换。配对让你能够实现眼前的目标,同时确保不会忽略项目的整体目标。它会保证有人在考虑下一步的走向和下一个要解决的问题。
虽然配对编程引起了很多争议,但是大多数优秀的开发人员还是在按照这一方法进行开发,至少有的时候是这样的。管理人员们可能会相信配对编程降低了生产效率,然而尽管开发小组的生产效率在一开始会有所降低,但是研究已经表明从质量和增加的生产效率的角度来看,配对编程远远超过了开发人员单独工作的质量和效率7。而另一方面,开发人员可能会觉得配对编程非常困难,因为它需要与人们更多的交互过程,并与另一个开发人员一起编写代码。但是这也是建立一种相互学习的环境的最好方法。
实施配对编程
1. 不要独断专行——要讨论。与你的小组成员讨论配对编程的思想及其优劣,而不是独断专行地给他们定规则。配对编程是开发人员相互学习的绝好机会。
2. 确定你的小组需要多少配对。配对编程是一项工作强度很大但是令人满意的工作方式。
3. 不要让配对编程人员每天连续工作八个小时——否则你的小组会吃不消的。从较短的时间开始——每天一到两个小时,看看它是如何进展的,然后随着小组信心的增强而延长时间。
4. 定期检查。如果你已经决定尝试再次进行敏捷开发,你就需要确保为小组营造了正式的环境,以便(定期)就项目进度进行反馈。
重要的成功因素
· 尝试它——如果你不去尝试,你就永远不了解它。
· 时间——给你小组(足够的)时间来尝试,并一步一步来完成。
· 沟通——配对编程会暴露一些有争议的问题——要保证沟通的渠道畅通。
· 配对恐惧症——你可能会碰到拒绝或者不希望与别人搭配工作的人。通常情况都是有别的原因驱使他们这样做,所以你需要找出并解决这些原因。
· 花时间思考——开发人员需要时间来思考并想出主意——确信给他们留出了时间做别的事情。
从整体上讲,我们在这里说的是要尝试这种方法——首先尝试测试-编码-重整循环,一旦你让它运转起来,就尝试一下配对编程。你应该马上就可以看到质量的提升,以及团队里沟通层次的提高。
我们在本文没有谈及的内容很简单——增量设计。敏捷编程喜欢简单的增量、改进的设计,而不是在编写代码之前的大型设计。很多人都认为敏捷编程不喜欢设计——事实并不是如此,而应该是只要满足最低需要就行了。
在本系列的第二部分里,我们将更加仔细地探讨简单设计以及一套的开发团队做法。
在《敏捷软件开发》上中下系列的上篇里,我们探讨了开发人员做法,也回顾了技术优势如何大幅提高软件质量。第一部分把重点主要放在了测试-编码-重整循环上。现在我们转到中间一个圆环,看看敏捷开发做法如何在小组这一层次发挥作用。
让小组高效工作——小组做法
一旦每个开发人员都在紧密围绕中心圆环的反馈循环工作时,我们就可以看看整个开发小组能够如何以更加敏捷的方式工作。小组这一层次的做法是敏捷开发的核心,因为它们能够显示出小组成员可以如何更加高效地一起工作并推动共同进行技术决策。我们将分别从四个方面来讨论小组的这种改变——设定基调、基于小组的代码编写标准、提高和保持效率、采用“统一小组”方式(包括与开发小组直接相关的东西)的首要步骤。我们给你举的例子来自于3Q Solutions公司,这是一家生产财物管理系统并完全使用敏捷开发方法的软件公司。
设定基调——第一步
敏捷软件开发的一个中心思想是小组朝着一个共同的目标工作。尽管很多流程都提倡小组工作,但是敏捷开发(真正)融合了支持小组工作的做法,并将小组工作放到了日常做法里。在开始讨论小组做法之前,我们需要先为小组设定一个基调,让他们开始感觉更像是一个真正的小组。
开放的工作空间
为更加开放的、基于小组的敏捷开发方式设定基调的一种最佳方法是为小组创造一种开放的工作空间(open workplace)。这意味着要建立一个或多个开放的区域,并尽最大可能进行沟通和合作。你想要专门了解什么样的环境能够让配对编程更容易。小格间和办公室是与敏捷开发开放工作空间格格不入的,所以应该避免其出现。在一家与Exoftware有合作关系的公司里,开放空间区域只被用于工作,里面只有用于配对编程、集成和构建软件的机器。其它的所有区域都留给带有Internet连接和电话的个人计算机。如果你有这样的空间,这就是应该考虑的东西,因为它有助于清楚地表明“当我们在工作区的时候,我们在工作”。
不要低估开放的工作空间对于小组的重要性——这就是为什么我们将其作为第一步的原因。下面的一幅照片就是是3Q Solutions开发小组的工作空间。
请注意,两张大桌子(下面没有文件柜)被摆在一起,构成了最适合配对编程的办公桌。
集体主义主人翁精神
我们想要介绍的下一个思想是集体主义主人翁精神(Collective Ownership)。敏捷编程的这种中心思想是让每一个人都对整个系统负责,每一个人都有更改代码的自由。这是一种重要的思维方法,因为它让小组的注意力都集中到了项目上,从而确保有一个共同的目标。与配对编程相关的其它步骤也强调这种思想,但是尽早引入这种思想是非常好的。
简单设计
敏捷开发崇尚简单的渐进设计,而不是剧烈的颠覆式设计。其目标是(首先)只指设计我们所了解的项目的那些部分,仅此而已,然后让该设计随着时间的推移而逐渐改进,这有助于提高灵活性并将变化导致的成本最小化。
我们就从3Q Solutions公司举一个例子,有一个客户要求获得一个规则引擎(rules engine)。小组传统的做法是花上数月时间开发规则引擎,然后可能还是无法把它卖出去。在与客户共同协商的情况下,小组决定设计一个满足规则引擎工作要求的最简单系统,并为每一条规则创建一个瘦垂直系统(a thin vertical system)。这就给予了客户他们真正需要的东西——可证明的规则——并确保投资抵消了投入的时间。这样小组可以在保持灵活性的同时从一开始就不断改进设计。简单设计是一个复杂的领域,研究它的最佳方法是获得外部的帮助。
重要的成功因素
· 赞同——整个开发小组坚持尝试使用敏捷开发以及开发小组圆环里的做法极其重要。如果不能这样坚持,开始甚至保持这样的做法都是非常困难的。
· 沟通——这一点怎么强调都不够。保证小组里高层次的沟通和对诸如集体主义主人翁精神这样的概念的理解非常重要。
· 配对编程——配对编程为很多小组做法提供支持,并将加强小组的沟通和凝聚力。
· 行政——如果没有行政上的支持,创造开放工作空间将会非常困难。在某些情况下,当行政机构的官僚主义作风盛行的时候,我们只用进行一些改变就行了。
· 每日例会——这一个每天早上进行的简单会议,供开发人员讨论当日面临的工作和问题。这样的会议应该是站着开的,因为其时间不应该超过几分钟。
小组编写代码——第二步
既然我们已经安置好了工作空间,并设定了小组的基调,我们现在就需要看看小组是如何处理代码的。我们这里的目标是确保所有通过配对编程编写的代码都能无缝地集成在一起,并且符合小组所承认的标准。通过推动第二步的进行,我们为支持第一步还有很大一段路要走。
代码编写标准
无论你是否决定采用敏捷编程,代码编写标准(coding standard)是一个非常好的最佳做法。这一步骤涉及让小组创立一套他们能够完全理解和坚持使用的代码编写标准。代码编写标准给予我们下列优势:
· 它让我们能够轻松地读懂别人的代码,这样所有人都可以进行(代码)交换。
· 代码为未来接手的小组提供了一个绝佳的信息源,即使有小组成员离队。
· 新的小组成员有一套指导方针——而不是瞎猜。
大多数小组都会利用已有的框架,并围绕其构建自己的一套标准。这里的关键要素是开始,立即解决小组正在奋力解决的问题,然后根据需要向前推进。也不要为了标准而去强行推行标准——这毕竟是整个小组需要共同认可、相信和使用的东西。下面是3Q公司代码编写标准文档的一小段。
CamelCase
CamelCase里的一切、类名称都以大写字母开始,而方法和字段的名称则不需要。
任何内容都不要放在有大括号的那一行。
字段以下划线开头: |
_fieldname |
变量名不以下划线开头: |
variableName |
方法: |
public void methodName(String stringValue) |
接口公开
公共方法在类的最上部,后面跟有受保护的方法,然后才是私有方法。将所有继承自抽象类或者实现结构的方法都靠前放置,这是一个好主意。
尽可能做到立即就能找到一个类,并马上可以感觉到其功能以及它如何实现该功能,而不需要滚屏。
方法和类的名称
让其名称能够说明其功能。注意,不同的开发人员对于什么样的方法可读有不同的看法,他们更喜欢从周围的类,甚至是方法里的参数看出其作用。对这一点还存在争议,但是从名字来判断一个方法的作用是肯定可行的,因此:
doIForAllX()
就不理想,但是:
setupAllTableRowItems()
就很好。
而:
createRows()
可能更好。
[getVarvscalculateVar, 直接的getter对方法]
[不要将查询与作业混在一起]
方法的抽象
方法里的代码的抽象程度应该与同一个方法里其他所有代码的相同。这样的话,事件的自然过程能够被弄清楚。例如:
public void initializeDataBase()
{
_connection = createConnection ();
setUpTable();.
For (inti=0;i
}
你稍稍一瞥,不用费什么功夫就可以读懂它。我们在3Q的时候非常珍惜视力,所以把这段代码变成了几个清晰明了的步骤,就像下面这样:
public void initializeDataBase()
{
setUpConnection();
setUpTable();
setUpTableRow();
}
这就有可能:
1.感觉到事情进展得怎么样
2.很容易就浏览到我们希望找到的类的确切部分(如果我们对表格行的设置感兴趣,我们就按住Ctrl点击setUpTableRow())。
得墨忒尔法则(Law of Demeter,即最少知识法则)
类应该只能够访问那些可以直接从其字段或者变量访问到的方法。对送进来的对象或者类自行实例化的对象的参考也是如此。
一般情况下,不要这么做……
publicintcalculateRetirementFund()
{
return getClient().getRetirementDetails().getRetirementFund();
}
……而要这么做:
public void calculateRetirementFund (RetirementDetails details)
{
return details.getRetirementFund();
}
这有助于为类设定范围并减少不必要的方法调用和委派。
顺序选择迭代
一般可以将方法分为下面三种类型。一系列事件,一个接一个;对集合的搜索或过滤;以及对集合或者数组的迭代。
收集方法、向量创建、向量设置、向量功能(vector dosomething)
集合一次又一次地出现,每次都是同样的问题,主要同类型有关。如果在集合里有一个任意的运行库强制转换(casting),那么总有出现错误类型的机会,导致强制转换异常的出现。
让集合变成可以针对具体类型,这使得在编译的时候检查往集合里加入的内容成为可能,同时还让根据类型来适应自定义的集合方法变得更容易。
不要使用临时变量——用查询来替代临时变量
在有关重整的书上查找这个内容——“用查询来替代临时变量”,最好不要抱着临时变量不放,它会增加代码的复杂性,给阅读者带来困难,同时减少了对算法作进一步重整的可能性。
测试打破常规
过多的设置意味着不佳的模式。你应该只需要设置那些与你正在测试的类直接相关的对象。
尽量让单元测试精细化,这将带来可移植性更强的代码,并将它推向更加清晰、更加独立的实现。
通过回调制针测试
回调制针(backpointer)完全就是个麻烦事,应该避免其出现。它会带来相当多的异常,状态模式就是其中一个。一定要了解自己实现回调指针的理由。如果理由是“它会起作用”,那么你就在失去什么东西。
视图测试——将测试三要素实例化
在一个构造完好的应用程序里,视图层应该从域抽象出来,达到一种不需要创建视图就能够测试该应用程序的程度。不够精细的测试需要更加经常地更改。见上文测试打破常规。
这只是来自一个不断改进的小例子。我再提醒一遍,从简单的开始,保持其基本框架,得到所有人的同意。
连续集成
瀑布式方法的一个缺陷是代码库的集成往往每隔数周或者数月之久才进行一次。新的错误常常会随着代码的集成而不断暴露出来,我们不得不花额外的时间来更正错误并重新集成。如果集成不是频繁进行,那么反馈就不可能像应该的那么紧密。敏捷开发要求进行更加频繁的集成——在3Q的案例里,这意味着每天要集成一到两次。
大多数小组一般都会有一台构建计算机,成对的开发人员能够利用其检查在测试-编码-重整循环里编写好的代码。每对开发人员都有确信其代码在被集成到代码库之前就已经经过测试和重整。一旦检验完毕,自动化的构建计算机就会编译所有的代码,运行所有的测试,并通过显示器(向小组)显示出来——构建过程是否需要引起注意——例如:新加入的代码有没有破坏什么东西?
这会做两件事情:
· 从代码被集成(进代码库)到小组意识到存在问题之间的时间间隔会被减到最小。
· 构建显示器将信息传达给整个小组——不论是集成成功完成——还是需要引起注意——这让小组可以立即作出相应的反应。
像这样频繁的集成意味着软件的构建是不停进行的,任何人在任何时候都可以参与构建过程。构建过程需要被自动化,以便使集成尽可能地容易,这是十分重要的。下面就是3Q公司的构建监视器的向小组传达信息的一个例子。
就如上面图画所显示的,构建服务器能够向小组提供额外的信息。
重要的成功因素
o 自动化——这需要成为一个自动化的过程。否则你将不得不专门找一个开发人员来维持构建过程——这可不是一个有意思的工作。首先就要营造环境,取得设备和实现自动化。
o TCR和配对编程——对于这一层次的集成工作,小组必须按照测试-编码-重整循环来进行,这样他们才有信心保证所有的问题只会发生在集成过程里。如果没有TCR循环,这一部分的过程将会非常困难。
o 按部就班——就像这个小标题说的,不慌不忙地从简单的地方开始,然后随着时间的推移来逐步改进——尤其是在代码编写标准这一块。
保持高效率——第三步
就如我们在《上篇》里说的,敏捷开发过程是一项工作强度很大的编程方式。除此之外,软件开发本身就压力重重,而小组累垮的可能性非常高。
可持续的步伐意味着开发小组现在和未来的工作都将非常艰苦。加班不是我们希望鼓励的事情,尽管有的时候需要如此。如果小组不得不加班工作,那么我们想要尝试将可持续步伐里的加班时间控制在一到两周而不是一到两个月。再强调一遍,敏捷开发是一项强度很大的工作;配对编程要求很多交互和重视,测试-编码-重整循环也是如此。尽管敏捷开发会引发我们小组的最大潜能,但是我们需要清楚很多时候的大量加班会累垮整个小组的风险。
重要的成功因素
这是管理者必须十分清楚的一个领域。确保小组在整个项目里保持合理的步伐是其主要职责。
开始转移到统一小组——第四步
有的人可能认为Metaphor的概念应该来得更早一些,但是我们建议在这一阶段快结束的时候才引入它,因为这是我们首次提到客户/业务方(customer/business)。Metaphor是客户与开发人员之间系统的通用语言。它看起来可能不重要,但是以Exoftware的政府顾客为例,开发小组一般都把业务方(也就是定义系统需求的人)当作客户。但是对于业务方而言,“客户”指的是最终用户。这就导致开发人员和“业务方”之间的困惑和挫折。
Metaphor的作用不只是一门通用语言——它还与上下文和对系统是什么的高层次理解有关。在这里我们能够采取步骤做到真正地与我们的业务合作伙伴沟通并共享共同的目标。3Q公司使用一种叫做Adaptor Tree Hierarchy体系,它通过一门客户/业务方共同认可的语言给予开发人员一个广阔的系统视野。例如:
ThreeQData
· todaysDate
· marriage
o spouse
o economicindicators
o client
§ lossofincomestory
§ annualincome
§ coveramortisationeroision
§ ...
§ managedfundstory
§ pensionstory
这个树形结构的每一部分都可以扩展出更多细节,能够轻易地改变,并提供一个很好的系统视角,同为整个小组提供一门通用的语言。
重要的成功因素
· 坚持到底——只有当你坚持使用的时候Metaphor才会有效。它将会成为日常语言的一部分,但是适应它需要花时间。
· 从基本的开始——从Metaphor的基本框架开始,了解它,使用它,然后以此为基础来创建它。
· 取得帮助——让尽可能多的相关业务方/客户参与Metaphor的创建——让其他人尽早参与进来是至关重要的。
敏捷开发的小组做法的目的是帮助小组把重点放在集体工作上,并理解其共有的做法和目标。尽管有的做法,比如代码编写标准,能够在隔离的情况下完成,但是如果与具体的开发人员做法,例如测试-编码-重整和配对编程结合起来,那么这些小组做法将发挥最大效用。
本系列的最后一部分将探讨开发人员小组如何开始同客户方/业务构成“统一小组”。
在敏捷软件开发方法上中下系列的最后一篇文章里,我们将探讨开发小组如何与客户交互,如何让其参与到开发过程里来。
在《敏捷软件开发》上中下系列的上篇里,我们了解了开发人员做法以及技术优势如何带来质量的显著提高。在中篇里,我们探讨了开发小组做法以及如何建立一个效率最高的开发小组,并重点研究了代码编写标准、连续集成和用于描述系统的通用语言。现在,我们要看看最外面的圆环——“统一小组做法(one team practice)”,这其中包括开发人员、测试人员和客户——并帮助更好地协调业务和IT。
协调业务和IT——“统一小组”做法
敏捷软件开发里的统一小组指的是敏捷开发小组和所有的利益相关人为了一个共同的目标结成一个团队工作。尽管小组里的每个成员都必须把各自主要精力放在具体的任务上,但是小组更喜欢开放的、真诚的和频繁的沟通,而不是暗地里的操作。
统一小组强调由开发人员作出技术决定而由客户作出业务决定,一贯如此,毫无例外。高度的交流,例如每日例会以及项目辐射(在《中篇》里讨论过)会帮助增加交流并不断持续下去,以确保及时获得频繁的反馈信息。
这一概念对于将敏捷开发的所有元素集中到一起是必需的。
创建背景并取得需求——第一步
在你开始敏捷开发的这一部分之前,从客户、业务方和用户取得需求信息;他们才是定义需求的人。由于业务方在这些做法中扮演了至关重要的角色,所以他们必须完全理解自己在敏捷开发环境里的角色是什么,以及他们能够做到什么。让其高速运转起来肯定需要进行讨论会和其他培训工作。
在解释敏捷开发的时候,需要向业务人员阐明的重要优势有:
1. 能够,在任何时候,改变其对最小成本的观点。
2. 能够根据来自市场或其他地方的反馈进行调整和应变。
3. 在任何时候都知道项目的状态,并具备可预见能力。
4. 能够从业务的角度参与项目的指导工作。
重要的成功因素
· 理解——客户将需要某种程序的培训才能确切地理解他们在敏捷开发环境里扮演的角色。
· 沟通——以协作的形式与客户进行交谈和沟通是十分重要的。在整个项目过程中都应该这样,但是从一开始就坚持这样显得尤其重要。
客户/业务方介入——第二步
在这一步骤里,我们要通过用户的素材和验收测试让客户参与到开发过程里来。很多客户可能在编写用户素材或者验收测试上经验不多或者完全没有经验;再强调一次,可能需要某种程度的讨论会或者培训来帮助其完成任务。
用户的素材
用户的素材就是“需求”。每个素材都代表系统需要如何解决某个特定的问题。然而,用户的素材不是大量的写满需求的文档,而是写在素材卡上,应该作为实现更进一步谈话的引子。
好的素材需要什么?
客户,或者更加常见的客户小组,需要聚在一起,在一张5x3寸的素材卡上为系统编写用户素材。我们用财物管理软件公司3Q Solutions来作为例子:
“客户希望能够获得一个规则引擎,从而可以用规则来评估顾客的经济状态。”
这一要求或者素材存在的问题是太不明确。编写好素材卡的正确规则应该是INVEST:
独立的(Independent)
可协商的(Negotiable)
垂直的(Vertical)
可估计的(Estimable)
短小的(Small)
可测试的(Testable)
面的素材显然是不可估计的(很难判断它需要花多长时间)、不短小的(这是一个非常巨大的、不明确的要求),也是不可测试的(你如何能够对像这样的要求进行由测试驱动的开发工作?)。所以下面这样一个素材可能会更好:
“客户希望能够分析顾客当前拥有的现金量——太多、太少,还是刚刚好(取决于生活方式的成本和对风险的态度)。”
这一素材就满足了我们INVEST标准的所有要求。当这个素材在小组(客户和开发方)中讨论的时候,它很明显地就传达了客户真正需要的是具备说明规则引擎的能力。上面的例子表明,一条规则就足够说明用户的需要。这就是编写素材的方法。重要的是,素材要引发产生对话,而对话带来对客户需求的明确和真正理解。
沟通
要记住,素材的主导思想是,它们是发生更进一步对话的引子。其原因是语言要以上下文和理解为基础。没有提问,没有对话,我们将无法体会其中微妙的含义。我们就以Matt Cohn’s Buffalo这个短语为例子。Buffalo(布法罗市)是美国纽约州的一座城市,是野牛(bison)的同义词,还有动词“欺骗和困惑”的意思。所以这样一个句子“Buffalobuffalobuffalobuffalo”是成立的。或者更加明确一点就是来自(纽约州)布法罗市的野牛欺骗了其他的野牛(bison from Buffalo (NY) intimidate and confuse other buffalo)。所以如果没有上下文,这个短语就是毫无意义的。
在每张素材卡的背面,我们建议客户快速记下任何有关验收测试的想法。
验收测试
验收测试用来保证:
1. 客户确信给定的功能能够满足设计的要求。
2. 给予开发人员一个明确的停止点:当验收测试通过的时候,功能就被实现了。
在敏捷开发项目里,客户要编写所有的验收测试。在项目初期,开发人员可能需要与客户紧密合作,以编写验收测试的内容。
我们还建议你使用AT框架并将测试自动化。开人员人需要能够随着他们不断加入新功能而反复地运行这些测试。
下面就是与上述素材相关的AT框架的例子。
交互测试(示例)
//概述
“分析顾客的现金收支状况,考察他们在给定的生活方式成本和对风险的态度的条件下是否握有过多的现金。”
//设置顾客数据 |
|
|
UserClicksMainMenu |
MenuFinancialObjectives |
|
UserInputsText |
FinancialObjectivesAttitudeToRisk |
“3-低回报-长线投资” |
UserClicksMainMenu |
MenuCurrentBalanceSheet |
|
UserInputsText |
CurrentBalanceSheetTotalCash |
30000 |
UserClicksMainMenu |
MenuFinancialObjectives |
|
UserInputsText |
FinancialObjectivesLifestyleCost |
25000 |
//现金规则 |
|
|
TestValueOfText |
AnalyseObservation |
“如果担心风险,你应该维持不超过#12,500的现金结余。” |
TestValueOfText |
AnalyseRecommendation |
“考虑将#17,500从现金帐户转移到可投资的资产上。” |
TestValueOfText |
AnalyseDestination |
“查询投资本金总额,将多余的现金转移到现金存储帐户,除非用现金购买资产。” |
//hyperlink |
|
|
UserClicksControl |
AnalyseDestination |
|
TestValueOfLabel |
WorkAreaTitle |
“本金总额” |
在3Q公司,客户会编写验收测试,并以电子文本的形式每天提交给开发小组。所有的验收测试都会被尽早地提供给开发小组。这一过程与测试-编码-重整循环配合得相当好,它使得开发人员可以在进行验收测试失败之后,运行通过测试-编码-重整循环,然后重新运新验收测试,直到看到其通过测试。每个素材都可能多次进行验收测试,但是一旦所有的验收测试都通过了,那么该素材/功能的实现就完成了。
重要的成功因素
· 不慌不忙——用户素材不容易写好,所以在进行首批任务和讨论任务的时候给自己充裕的时间。
· 验收测试帮助——开发人员可能需要从一开始就与客户一起编写验收测试。专门为这一任务拨出时间——好的验收测试将带来不同寻常的收获。
· 寻求帮助——如果意识到你和你的小组需要帮助——去寻求帮助吧,不要犹豫!
· 已有的需求文档——如果有现成的需求文档,你要将它用作编写素材的基础。要记住,把这些文档当作“新的”素材。它们是对话的要点,而不是定好的要求。
策划——第三步
敏捷软件开发有三个层次的策划:
1. 高层次的发布策划,在这里策划项目的所有发布。这通常取决于项目的规模,但是某些项目的多次发布要求对长达18个月的期限的高层次策划。
2. 发布策划,第一次发布在这里被策划。每次发布之间的间隔为3个月。
3. 反复策划,通过其来策划下两个星期的工作。
这一三级策划过程的目的是让小组首先理解最终的目标,但是只详细策划他们现在所知的内容——未来两周的工作。
发布策划
在高层次发布策划阶段,客户和开发人员应该在一起共同讨论和理解整个系统。通常已经存在的需求文档能够用于启动这一讨论。在理想状况下,客户应该在开会的时候带上含有即将发布的大多数内容的素材卡。
在会议过程中,开发人员将需要估计素材的难度。这可以在会议过程中或者在会议之后进行。我们建议每个人相互比较各自素材,并把具有相同难度的素材集中到一起。然后,使用一个从最简单到最难的测量表,你就可以开始估计每个素材(的难度了)。小组使用不同的方法来给素材评分,按照难度分别打上1到10分。
现在客户能够策划最初的高层次发布计划了。高层次发布并不一定要十分精确,优先顺序和估计都不需要很可靠,但是它会为小组定下方向和提供决策的足够信息。
小型发布
下一步,客户需要拿走估计好的素材卡,并根据最近一个发布将素材的重要性的优先顺序排列好。客户需要考虑它们需要系统立即实现什么,因而这些素材将构成即将进行的发布。这些估计在这里变得十分重要,因为开发人员已经估计的是他们能够给定的发布时间里完成什么;(这个给定的时间)在大多数情况下是3个月。
短期发布循环可以保证紧密的反馈循环,还能让小组把精力放在与项目紧密相关的重要目标上。
反复策划
现在小组需要为未来两周制定具体的计划。再强调一次,客户必须将素材的优先顺序排列出来,详细说明他们希望在未来两周里看到的功能。
这些素材卡然后就被放到两周的反复(发布里)。最近的一次反复将是小组立即进行的工作。他们将交付这个反复,也就是全力工作、软件测试和取得反馈(即再次为未来两周策划),然后再次开始。如果素材在一个反复之前就完成了,开发人员会要求获得更多的素材。如果所有的素材都看起来是无法完成的,那么开发人员和客户要共同将素材移到下一个反复里或者适当地分割一下素材。
两周的反复让客户可以充分利用任何变化。例如,3Q公司碰到了一个很有预见能力的客户。他意识到一个按计划放在发布后期的素材事实上需要更早完成。在经过一个简短的讨论之后,小组用客户要求的素材替换掉了当前发布里具有同等价值的素材。那么成本呢?只是一个15分钟的对话。
以上只是对策划过程如何工作的简要概述。我们建议寻求对该过程这一部分的一些帮助或者指导,因为它可能会十分复杂,仔细调整常常也是必需的。
这一反复过程和发布策划分别要每两个星期和每三个月进行一次。
重要的成功因素
· 在反复中期进行一次检查——尽早检查小组在反复中期的进展情况。
· 估计就是这样——小组一开始的估计常常会偏离甚远——开发人员都是乐观主义者!但是随着小组进展到新的反复并适应这一过程,估计(的准确性)或者速度(小组工作有多快)就会确定下来。
· 昨天的天气——一旦完成了一个反复,你将对小组的速度有一个粗略的概念——两个星期里可以交付多少素材。这就是小组认可的在未来两周里的速度和小组工作量。随着小组的成熟,具备更好地进行估计的能力,你的速度可能会提高,然后固定在一个稳定的速度上。
· 速度不是一根棍子——而是对管理者的提醒——速度不是用来鞭打小组的大棒;它是用来测量自然波动的。
· 决策——客户或者客户小组必须具有决策权,或者能够迅速进行决策,尤其是在需要变化或者适应的时候。
· 协商的意愿——客户必须愿意就范围等内容进行协商。这才是敏捷开发的工作方式:就范围进行协商,排列最具业务价值的功能的优先顺序。
敏捷开发里的策划可能会很困难,所以我们建议你去寻求一些帮助,并花时间来完成它。
保持高效——第四步
逐步推进这一过程的最佳方法之一是有一个在现场的客户。最理想的方法是让客户坐在小组成员当中,这样就可以随时回答问题。这限制了开发人员的随意猜想。此外,在现场的客户能够以最快的速度回答开发人员的疑问。
这并不意味着这个客户不去从事他的“日常”工作,而是说他就在周围准备好回答问题。即使隔着一层楼也会影响沟通。要进行面对面的对话,而不是用电话或者电子邮件。
显然,设置现场客户并不总是可能的,在这种情况下,他应该尽可能地接近小组,并尽可能地参加每日例会。如果这也不可能,那么你就要让他参加日常会议——至少一周一次——以确保你在不断地去的反馈意见和沟通。
对反馈和沟通的增加也需要定期进行回顾。这最好应该在每个反复结尾的时候进行。这样的回顾能够让小组有机会坐下来检查上一个反复,并弄清楚什么做得好、什么做得不好,以及下一次能够把什么做得更好。应该问三个问题:什么有用?什么没有用?我们要改进什么?
重要的成功因素
· 现场与否?——现场客户或许会带来一些问题,但是如果可能的话还是要找一个现场客户。如果无法实现,就要寻找其他的途径来确保定期的沟通。
· 回顾——把在每次反复结束的时候进行回顾作为一条纪律定来下,并把人们的想法付诸行动。
我们刚刚更加仔细地探讨了《上篇》里第一个图表的外层圆环,它需要所有参与者的同意。这可能是敏捷开发里最困难的一部分,但是它能够很好地协调业务和IT,而且其好处不仅对于业务而且对于IT也是很有价值的。
总结
尽管在本系列里我们向你讲解了如何一步步地培养敏捷软件开发的能力,以及如何从内到外树立开发人员的信心,然后是开发小组的信心,最后是整个项目小组的信心。从在Exoftware公司的经验可以看出,很多公司都选择为某个项目建立一个完整的敏捷开发实验小组,并让一个指导老师手把手地帮助小组。如果你选择这一方法,你将具有从所有做法直接获得好处的优势,此外,它将给你适应你具体环境的有价值的信息。简单地说有:
实验性的敏捷软件开发——如何开始
你的目标是什么?
评估你现在所处的位置以及你想要去哪里,这对于使用敏捷开发做法来说是至关重要的。这将帮助你确定希望取得的预期成果。对其的外部评估常常也是很有用的,因为它们将为处理你的问题提供一个客观的视角。
实验性的敏捷开发
虽然我们已经叙述了开发敏捷开发的一种方法,但是在一个项目上引导实现敏捷开发是理解敏捷开发方法是否适用于你的机构的最佳方法,它还会帮助你了解如何适应自己的环境。
测量标准
如果可能的话,你要在项目开始前或者在实现敏捷开发做法之前收集一些测量标准。即使这些标准来自于其他的项目,它们也将有助于为敏捷开发已经实现的内容提供一个良好的基准。你还要确保能够在敏捷开发项目过程中以及之后收集到一些高标准的测量标准。缺陷率、测试内容或者最终期限都是很好的且简单易行的高标准测量标准。
环境
要明白实验性的敏捷开发可能要求对你的物理环境进行一些改变。例如,开放的工作空间是敏捷开发真正起效的必要条件。
寻求帮助
外部的帮助能够指导你的实验性项目迈向成功。它能够帮助你理解你在哪里以及你想去哪里,并且能够向你指明如何让敏捷开发适应你的环境,从而到达这一目标。此外,外部帮助可以确保小组集中精力回答随时出现的问题。为将敏捷开发应用到其他工程小组里而树立一个业务案例也是十分重要的。
Brian Swan是Exoftware公司教授敏捷开发的指导老师。他在敏捷开发的技术和管理方面具有相当丰富的经验,曾经带领很多小组成功地转换到了敏捷开发,并以敏捷开发的思想和做法来培训开发人员和管理人员。他在Exoftware公司和在敏捷开发方面的工作使他到过很多公司,并对其开发小组产生了持续的、积极的影响。Brian先前的经验还包括担任Napier大学的讲师,讲授软件开发和人机互动。Brian可以通过电子邮件联系上