`
xhy0422
  • 浏览: 281132 次
社区版块
存档分类
最新评论

敏捷软件开发

阅读更多

敏捷软件开发<o:p></o:p>

| 2006-08-10 04:03:48 (530 次阅读) <o:p></o:p>

作为三篇系列文章的第一篇,我们将带你了解敏捷软件开发的重要做法——如何使用它们、你可能会碰到什么样的问题,以及你将从它们那里获得什么。

敏捷软件开发不是一个具体的过程,而是一个涵盖性术语(umbrella term),用于概括具有类似基础的方式和方法。这些方法,其中包括极限编程(Extreme Programming)、动态系统开发方法(Dynamic System Development Method)、SCRUMCrystalLean等,都着眼于快速交付高质量的工作软件,并做到客户满意。

尽管构成这个敏捷开发过程的每种方法都具有类似的目标,但是它们实现这个目标的做法(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);

}

}


我们所有的测试都通过了,而且我们没有看到任何重复或者其他必要的重整,所以应该是时候进行下面的测试了。然而事实却不是这样的。我们topremove方法的实现里有一个潜在的问题。如果对一个空的Deck调用它们,会发生什么?这两个方法都会从纸牌的内部列表里跳出一个IndexOutOfBoundsException异常,但是目前我们还没有就这个问题进行沟通。回头看看简单性的原则,我们知道自己需要沟通。我们的类的用户应该知道这个潜在的问题。幸运的是,我们将这种测试当作是一种沟通的方式,因此我们增加了下面的测试。 <o:p></o:p>

public void testTopOnEmptyDeck() {

<o:p> </o:p>

Deck deck = new Deck();

<o:p> </o:p>

try {

<o:p> </o:p>

deck.top();

<o:p> </o:p>

fail("IllegalStateException not thrown");

<o:p> </o:p>

} catch(IllegalStateException e) {

<o:p> </o:p>

assertEquals("Cannot call top on an empty deck", e.getMessage());

<o:p> </o:p>

}

<o:p> </o:p>

}

<o:p> </o:p>

public void testRemoveOnEmptyDeck() {

<o:p> </o:p>

Deck deck = new Deck();

<o:p> </o:p>

try {

<o:p> </o:p>

deck.remove();

<o:p> </o:p>

fail("IllegalStateException not thrown");

<o:p> </o:p>

} catch(IllegalStateException e) {

<o:p> </o:p>

assertEquals("Cannot call remove on an empty deck", e.getMessage());

<o:p> </o:p>

}

<o:p> </o:p>

}

<o:p> </o:p>

上面都是异常测试(Exception Test2)的例子。我们再一次运行这些测试看它们失败,然后加入实现让它们通过。

<o:p> </o:p>

public int top() {

<o:p> </o:p>

if(isEmpty())

<o:p> </o:p>

throw new IllegalStateException("Cannot call top on an empty deck");

<o:p> </o:p>

return ((Integer) cards.get(0)).intValue();

<o:p> </o:p>

}

<o:p> </o:p>

public void remove() {

<o:p> </o:p>

if(isEmpty())

<o:p> </o:p>

throw new IllegalStateException("Cannot call remove on an empty deck");

<o:p> </o:p>

cards.remove(0);

<o:p> </o:p>

}

<o:p> </o:p>

尽管guard语句有重复,但是我们决定不去管它,没有将它们简化成一个共同的方法。这是因为沟通的价值超过了重复的代价,当然这只是一个个人的选择。

<o:p> </o:p>

一手牌

我们已经完成了对牌桌和投注台面的测试和实现,现在就到了创建一手牌的时候了。待办事项列表再一次发挥其作用,我们得到了下面这样一个列表:

<o:p> </o:p>

创建一个一开始没有纸牌的空手

向手上加入纸牌

检查一只手是否击败了另一手

检查一只手是否爆了

<o:p> </o:p>

<o:p> </o:p>

为空手增加一个测试很简单,我们继续到给手上加入纸牌。

<o:p> </o:p>

public void testAddACard()

{

Hand hand = new Hand();

hand.add(10);

assertEquals(1, hand.size());

hand.add(5);

assertEquals(2, hand.size());

}

<o:p> </o:p>

我们运行测试,然后加入实现。

<o:p> </o:p>

public void add( int card )

{

cards.add(new Integer(card));

}

<o:p> </o:p>

测试通过了,我们没有看到Hand类里有任何重复。但是我们刚刚给Hand加上的实现和给Deck加上的方法极其相似。回头看看牌桌的待办事项列表,我们记得必须检查牌桌(上纸牌的张数)是否正确,我们最后也对手做同样的事情。

<o:p> </o:p>

public void testAddInvalidCard() {

<o:p> </o:p>

Hand hand = new Hand();

<o:p> </o:p>

try {

<o:p> </o:p>

hand.add(1);

<o:p> </o:p>

fail("IllegalArgumentException not thrown");

<o:p> </o:p>

} catch(IllegalArgumentException e) {

<o:p> </o:p>

assertEquals("Not a valid card value 1", e.getMessage());

<o:p> </o:p>

}

<o:p> </o:p>

try {

<o:p> </o:p>

hand.add(12);

<o:p> </o:p>

fail("IllegalArgumentException not thrown");

<o:p> </o:p>

} catch(IllegalArgumentException e) {

<o:p> </o:p>

assertEquals("Not a valid card value 12", e.getMessage());

<o:p> </o:p>

<o:p> </o:p>

}

<o:p> </o:p>

}

<o:p> </o:p>

我们加入了下面的实现来通过测试。

<o:p> </o:p>

public void add( int card )

{

if(card < 2 || card > 11)

throw new IllegalArgumentException("Not a valid card value " + card);

cards.add(new Integer(card));

}

<o:p> </o:p>

但是现在我们在DeckHand里有相同的guard语句,用来检查该自变量是否代表着正确的纸牌值。简单性的原则要求我们删除重复,但是在这里情况并不像Extract Method重整6这么简单。如果我们看到多个类之间存在重复,这意味着我们缺失了某种概念。在这里我们很容易就看到Card类担负起了判断什么值是有效的责任,而DeckHand作为Card的容器变得更具沟通性。

<o:p> </o:p>

我们引入了Card类以及相应的DeckHand重整,如列表B

<o:p> </o:p>

public class Card {

<o:p> </o:p>

private final int value;

<o:p> </o:p>

public Card( int value ) {

<o:p> </o:p>

if( value < 2 || value > 11 )

<o:p> </o:p>

throw new IllegalArgumentException( "Not a valid card value " + value );

<o:p> </o:p>

this.value = value;

<o:p> </o:p>

}

<o:p> </o:p>

public int getValue() {

<o:p> </o:p>

return value;

<o:p> </o:p>

}

<o:p> </o:p>

}

<o:p> </o:p>

public class Deck {

<o:p> </o:p>

private static final int[] NUMBER_IN_DECK = new int[] {0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 16, 4};

<o:p> </o:p>

<o:p> </o:p>

public void add( Card card ) throws IllegalStateException {

<o:p> </o:p>

if(NUMBER_IN_DECK[card.getValue()] == countOf(card))

<o:p> </o:p>

throw new IllegalStateException("Cannot add more cards of value " + card.getValue());

<o:p> </o:p>

cards.add(card);

<o:p> </o:p>

}

<o:p> </o:p>

public Card top() {

<o:p> </o:p>

if(isEmpty())

<o:p> </o:p>

throw new IllegalStateException("Cannot call top on an empty deck");

<o:p> </o:p>

return (Card) cards.get(0);

<o:p> </o:p>

}

<o:p> </o:p>

<o:p> </o:p>

private int countOf(Card card) {

<o:p> </o:p>

int result = 0;

<o:p> </o:p>

for(Iterator i = cards.iterator(); i.hasNext(); ) {

<o:p> </o:p>

Card each = (Card) i.next();

<o:p> </o:p>

if(each.getValue() == card.getValue())

<o:p> </o:p>

result++;

<o:p> </o:p>

}

<o:p> </o:p>

return result;

<o:p> </o:p>

}

<o:p> </o:p>

}

<o:p> </o:p>

public class Hand {

<o:p> </o:p>

<o:p> </o:p>

public void add( Card card ) {

<o:p> </o:p>

cards.add(card);

<o:p> </o:p>

}

<o:p> </o:p>

<o:p> </o:p>

}

<o:p> </o:p>

测试-编码-重整循环的每一阶段都涉及不同类型的思想。在测试阶段,重点放在了被实现的类的接口上。编写代码是为了让测试尽可能快地通过测试。而重整阶段可以被当作是使用简单性原则进行指导的微型代码审查。有没有重复的或者看起来类似的代码,不仅仅是在当前的类里,而且是在系统的其他类里?现在的实现可能会出现什么问题,类的用户能够与之顺利沟通吗?

重要的成功因素

<o:p> </o:p>

小步前进——TCR对于开发人员来说不是一个很容易的转换。一次只进行一个步骤,同时还要明白它学习起来有一定难度。

严格遵守原则——只进行TDD或者只进行重整并不能让整个TCR循环一蹴而就。给自己足够的时间来尝试,并取得效果。压力和最终期限会迫使小组回到原来的习惯上——一定要小心!

重整过程——与小组的所有成员交换意见,了解一下他们的反馈

理解——确保整个小组都完全理解TCR循环是什么,如何实现它。考虑一下就此主题进行员工培训和讲座。

<o:p> </o:p>

配对编程——第二步

TCR循环可以由某个开发人员独自完成,但是敏捷开发和TCR循环的真正威力来自于配对编程(pair programming)。在敏捷开发里,开发人员每两人一组编写所有的生产代码,其中一人担当“驱动者(driver)”(负责操作鼠标和键盘),而另一个人同驱动者一道解决问题和规划更大的图景。编程配对里的这个驱动者可以按需要进行轮换。配对让你能够实现眼前的目标,同时确保不会忽略项目的整体目标。它会保证有人在考虑下一步的走向和下一个要解决的问题。

<o:p> </o:p>

虽然配对编程引起了很多争议,但是大多数优秀的开发人员还是在按照这一方法进行开发,至少有的时候是这样的。管理人员们可能会相信配对编程降低了生产效率,然而尽管开发小组的生产效率在一开始会有所降低,但是研究已经表明从质量和增加的生产效率的角度来看,配对编程远远超过了开发人员单独工作的质量和效率7。而另一方面,开发人员可能会觉得配对编程非常困难,因为它需要与人们更多的交互过程,并与另一个开发人员一起编写代码。但是这也是建立一种相互学习的环境的最好方法。

<o:p> </o:p>

<o:p> </o:p>

<o:p> </o:p>

实施配对编程

<o:p> </o:p>

1.不要独断专行——要讨论。与你的小组成员讨论配对编程的思想及其优劣,而不是独断专行地给他们定规则。配对编程是开发人员相互学习的绝好机会。

<o:p> </o:p>

2.确定你的小组需要多少配对。配对编程是一项工作强度很大但是令人满意的工作方式。

<o:p> </o:p>

3.不要让配对编程人员每天连续工作八个小时——否则你的小组会吃不消的。从较短的时间开始——每天一到两个小时,看看它是如何进展的,然后随着小组信心的增强而延长时间。

<o:p> </o:p>

4.定期检查。如果你已经决定尝试再次进行敏捷开发,你就需要确保为小组营造了正式的环境,以便(定期)就项目进度进行反馈。

<o:p> </o:p>

重要的成功因素

<o:p> </o:p>

尝试它——如果你不去尝试,你就永远不了解它。

时间——给你小组(足够的)时间来尝试,并一步一步来完成。

沟通——配对编程会暴露一些有争议的问题——要保证沟通的渠道畅通。

配对恐惧症——你可能会碰到拒绝或者不希望与别人搭配工作的人。通常情况都是有别的原因驱使他们这样做,所以你需要找出并解决这些原因。

花时间思考——开发人员需要时间来思考并想出主意——确信给他们留出了时间做别的事情。

分享到:
评论

相关推荐

    敏捷软件开发.pdf

    敏捷软件开发是一种以人为核心、迭代、循序渐进的软件开发方法。它强调快速和灵活地响应变化,以适应不断变化的需求。敏捷方法反对繁重的文档和过度的预设计,提倡可适应性、可持续性和持续的客户合作。 书中,...

    敏捷软件开发原则、模式与实践.pdf

    《敏捷软件开发原则、模式与实践》一书是由著名软件开发专家、软件工程大师Robert C. Martin所著。这本书自出版以来,就被视为敏捷开发领域内的经典之作,对于软件开发人员、项目经理以及软件项目领导者来说,它提供...

    Scrum敏捷软件开发过程.pdf

    Scrum是一种敏捷软件开发框架,它强调灵活性、协作和快速响应变化的能力。Scrum的核心理念是通过短期迭代(称为Sprints)和跨职能团队的工作来不断交付可用的软件,并在整个过程中密切与利益相关者合作。 **敏捷...

    敏捷软件开发书籍 pdf

    敏捷软件开发书籍合集,包括: [Scrum敏捷软件开发] [The.Pragmatic.Bookshelf开发丛书-敏捷开发回顾:使团队更强大] [The.Pragmatic.Bookshelf开发丛书-敏捷开发指导] [敏捷开发修炼之道] [用户故事与敏捷方法]

    敏捷软件开发:原则、模式与实践清晰扫描中文版PDF(503页完整版)

    本书《敏捷软件开发:原则、模式与实践》是由全球知名的软件开发专家和软件工程大师Robert C. Martin所著,该书是关于敏捷开发与极限编程的综合性、实用性指南。书中深入探讨了软件开发人员、项目经理以及软件项目...

    敏捷软件开发:原则、模式与实践(带书签+源码)

    《敏捷软件开发:原则、模式与实践》是一本深度探讨敏捷开发理念和技术的权威著作,由业界知名专家Robert C. Martin(简称Uncle Bob)撰写。这本书不仅提供了丰富的理论知识,还结合实际案例,深入浅出地介绍了如何...

    敏捷软件开发实践估算与计划 Mike Cohn

    《敏捷软件开发实践估算与计划》是Mike Cohn的一部著作,由清华大学出版社于2016年出版。这本书深入探讨了在敏捷开发环境中如何进行有效的估算和计划,旨在帮助团队提升开发效率和项目成功率。 1. **敏捷开发**:...

    敏捷软件开发知识体系

    敏捷软件开发知识体系是中国敏捷软件开发联盟在2011年推出的一项重要工作,旨在采集国内企业敏捷成功实践,对敏捷软件开发进行深入研究。在软件开发领域,敏捷方法提供了一种与传统瀑布式方法不同的开发哲学和实践,...

    敏捷软件开发 原则模式与实践

    《敏捷软件开发:原则、模式与实践》是Robert C. Martin(简称Uncle Bob)的一部经典著作,这本书深入探讨了敏捷开发的理念、方法和工具,尤其针对C#编程语言进行了详细阐述。作为一本实践导向的技术书籍,它旨在...

    敏捷软件开发:原则、模式与实践(Agile.software.development:Principles,Patterns,and.Practices)中英版

    《敏捷软件开发:原则、模式与实践》是Robert C. Martin(也被业界称为Uncle Bob)的经典著作,这本书深入探讨了敏捷开发的核心理念,并通过实际案例介绍了如何在项目中运用这些原则、模式和最佳实践。这本书分为两...

    敏捷软件开发:原则、模式与实践(全)

    敏捷软件开发:原则、模式与实践(全) 敏捷软件开发:原则、模式与实践(全) 敏捷软件开发:原则、模式与实践(全) 敏捷软件开发:原则、模式与实践(全) 敏捷软件开发:原则、模式与实践(全)

    敏捷软件开发Agile介绍PPT课件.pptx

    "敏捷软件开发Agile介绍PPT课件.pptx" 敏捷软件开发是当前软件开发中最流行的开发方法之一,旨在快速响应客户需求,提高软件开发效率和质量。该PPT课件对敏捷软件开发进行了详细的介绍,涵盖了敏捷软件开发的历史...

    敏捷软件开发原则、模式与实践第19章C++源码

    在本资源中,我们主要探讨的是敏捷软件开发的原则、模式与实践,特别是在C++编程语言中的应用。这一主题源于《敏捷软件开发》一书的第19章,该章节通过一个具体的薪水支付案例来阐述敏捷开发的方法。在这个案例中,...

    敏捷软件开发:原则、模式与实践(全).pdf

    敏捷软件开发是一种以人为核心、迭代、循序渐进的软件开发方法。Robert C. Martin,作为敏捷开发的权威和实践者,通过《敏捷软件开发:原则、模式与实践》一书,向读者展示了一系列核心原则、模式和实践经验,旨在...

    敏捷软件开发:原则、模式与实践(带书签,源码)

    《敏捷软件开发:原则、模式与实践》是一本深度探讨敏捷开发理念、方法和技术的权威著作。这本书由著名软件开发专家Robert C. Martin撰写,旨在帮助开发者和团队更有效地进行软件开发,提升软件项目的成功率。书中...

    《敏捷软件开发》源代码

    《敏捷软件开发——原则、模式与实践》是软件工程领域一本经典的著作,它深入探讨了敏捷方法论在软件开发中的应用。源代码是书中理论与实践相结合的重要载体,提供了具体的实现示例,帮助读者更好地理解和掌握敏捷...

Global site tag (gtag.js) - Google Analytics