第5章 敏捷反馈

一步行动,胜过千万专家的意见。——Bill Nye,The Science Guy科普节目主持人

敏捷项目中,我们小步前进,不停地收集反馈,时刻矫正自己。但是,这些反馈都是从何而来呢?

第4章 交付用户想要的软件 中,我们讨论了与用户一起紧密工作——从他们那里获得反馈,并且采取实际的行动。本章中,我们主要讨论如何从其他渠道获得反馈。按照Bill Nye的观点,实践是绝对必需的。我们会遵循这一原则,确保你明确知道项目的正确状态,而不是主观臆测。

很多项目,都是因为程序代码失控而陷入困境。修复bug导致了更多的bug,从而又导致了更多的bug修复,成堆的测试卡片最后会把项目压垮。这时,我们需要的是经常的监督——频繁反馈以确保代码不会变坏,如果不会更好,至少能像昨天一样继续工作。在第78页,介绍如何让守护天使来监督你的代码。但是,这也不能防止你设计的接口或API变得笨重和难用。这时,你就要先用它再实现它(从第82页开始介绍)。当然,不是说一个单元测试在你的机器上能运行,就意味着它可以在其他机器上运行。从第87页开始,可以看到为什么不同环境,就有不同问题。

现在,你拥有了设计良好的API和干净的代码,就可以看看结果是否符合用户的期望了。你可以通过自动验收测试来保证代码是正确的,并且一直都是正确的。我们从第90页开始谈论这个话题。人人都想清楚了解项目的进度状况,但又很容易误入歧途,要么是被一些难懂的指示器误导,要么就是错误迷信华丽的甘特图、PERT图或者日历工具。其实,你想要的是能度量真实的进度,我们会在第93页介绍它。尽管,我们已经谈论了在开发的时候,与用户一起工作并及时得到用户的反馈,但是在其他的比如产品发布之后的很长一段时间,你还是需要再倾听用户的声音,我们会在第96页详细解释。

19 守护天使

“你不必为单元测试花费那么多时间和精力。它只会拖延项目的进度。好歹,你也是一个不错的程序员——单元测试只会浪费时间,我们现在正处于关键时刻。”

代码在快速地变化。每当你手指敲击一下键盘,代码就会被改变。敏捷就是管理变化的,而且,代码可能是变化最频繁的东西。

为了应对代码的变化,你需要持续获得代码健康状态的反馈:它是在做你期望的事情吗?最近一次修改有没有无意中破坏了什么功能?这时,你就带上守护天使,确保所有功能都能正常工作。要做到这样,就需要自动化单元测试。

编写能产生反馈的代码 Coding feedback

现在,一些开发者会对单元测试有意见:毕竟,有“测试”这个词在里面,毫无疑问这应该是其他人的工作。从现在开始,忘掉“测试”这个词。就把它看作是一个极好、编写能产生反馈的代码的技术。

先回顾一下,在过去大部分开发者是如何工作的:你写了一小块代码,然后嵌入一些输出语句,来看一些关键变量的值。你也许是在调试器中或者基于一些桩(stub)程序来运行代码。你手工查看所有的运行结果,来修复发现的所有问题,然后扔掉那些桩代码,或者从调试器中退出,再去解决下一个问题。

敏捷式的单元测试正是采取了相同、相似的过程,并且还让其更上一层楼。不用扔掉桩程序,你把它保存下来,还要让其可以自动化地持续运行。你编写代码来检查具体值,而不是手工检查那些感兴趣的变量。

用代码来测试变量的具体值(以及跟踪运行了多少个测试),已经是非常普遍的做法。你可以选择一个标准的测试框架,来帮助你完成简单的编写和组织测试的工作,如Java的JUnit、C#或.NET的NUnit、测试Web

Service的HttpUnit,等等。实际上,对任何你可以想象到的环境和语言都有对应的单元测试框架,其中的大部分都可以从http://xprogramming.com/software.htm上的列表中找到。

读者David Bock告诉了我们下面这个故事:

“我最近在设计一个特大项目中的一个功能模块,把构建工具从Ant迁移到Maven。这是在产品中已使用的、没有任何问题的及经过良好测试的代码。我不停地工作,一直到深夜,一切都在控制之中。我修改了一部分构建过程,忽然得到了单元测试失败的警告。我花了很多时间,来查找为什么修改的代码会导致测试失败。最后我放弃了,回滚了修改的代码,但测试仍然失败。我开始研究测试代码,才发现失败的原因是,测试依赖一个计算次数的工具,而且它还返回一个日期实例,日期设置为第二天中午。我又看了看测试,发现它居然记下了测试执行的时间,并将其作为参数传递给另外一个测试。这个方法有个愚蠢的差一错误(off-by-one),如果你是在夜里11点到12点间调用这个方法,它真正的返回值仍然是当天中午,而不是明天。”

从上面的故事中,我们学到了很重要的一课。

  • 确保测试是可重复的。使用当前的日期或者时间作为参数,会让测试依赖运行时间,使用你自己机器上的IP地址同样会让它依赖运行时的机器,等等。
  • 测试你的边界条件。11:59:59和0:00:00都是不错的日期测试边界条件。
  • 不要放过任何一个失败的测试。在前面的案例中,一个测试一直失败了,但是因为一段时间内每天都会有几十个测试失败,没有人会注意到这个伪随机失败。

只要有了单元测试,就要让它们自动运行。也就是每次编译或者构建代码的时候,就运行一次测试。把单元测试的结果看作是和编译器一样——如果测试没有通过(或者没有测试),那就像编译没有通过一样糟糕。

接下来就是在后台架设一个构建机器,不断获得最新版本的源代码,然后编译代码,并运行单元测试,如果有任何错误它会让你及时知道。结合本地单元测试,运行每个编译,构建机器不断编译和运行单元测试,这样你就拥有了一个守护天使。如果出现了问题,你会立刻知道,并且这是最容易修复(也是成本最低)的时候。一旦单元测试到位,采用这样的回归测试,你就可以随意重构代码。可以根据需要进行实验、重新设计或者重写代码:单元测试会确保你不会意外地破坏任何功能。这会让你心情舒畅,你不用每次写代码的时候都如履薄冰。单元测试是最受欢迎者的一种敏捷实践,有很多图书和其他资料可以帮你起步。如果你是一个新手,建议阅读《单元测试之道》(有Java[HT03]和C# [HT04]版本)。如果要进一步了解测试的一些窍门,可以看一下JUnit Recipes[Rai04]。

如果想要自动化地连接单元测试(和其他一些有用的东西),可以阅读《项目自动化之道》[Cla04]。尽管它主要是关于Java的,但也有类似的可以用于.NET环境或者其他环境的工具。如果你仍然在寻找开始单元测试的理由,下面有很多。

  • 单元测试能及时提供反馈。你的代码会重复得到锻炼。但若修改或者重写了代码,测试用例就会检查你是否破坏了已有的功能。你可以快速得到反馈,并很容易地修复它们。
  • 单元测试让你的代码更加健壮。测试帮助你全面思考代码的行为,帮你练习正面、反面以及异常情况。
  • 单元测试是有用的设计工具。正如我们在实践20中谈论到的,单元测试有助于实现简单的、注重实效的设计。
  • 单元测试是让你自信的后台。你测试代码,了解它在各种不同条件下的行为。这会让你在面对新的任务、时间紧迫的巨大压力之下,找到自信。
  • 单元测试是解决问题时的探测器。单元测试就像是测试印制电路板的示波镜。当问题出现的时候,你可以快速地给代码发送一个脉冲信号。这为你提供了一个很自然的发现和解决问题的方法(见习惯35,第136页)。
  • 单元测试是可信的文档。当你开始学习新API的时候,它的单元测试是最精确和可靠的文档。
  • 单元测试是学习工具。在你开始学习新API的时候,可以为这个API写个单元测试,从而加深自己的理解。这些学习用的测试,不仅能帮助你理解API的行为,还能帮助你快速找到以后可能引入的、无法与现有代码兼容的变化。

使用自动化的单元测试。好的单元测试能够为你的代码问题提供及时的警报。如果没有到位的单元测试,不要进行任何设计和代码修改。

切身感受

你依赖于单元测试。如果代码没有测试,你会觉得很不舒服,就像是在高空作业没有系安全带一样。

平衡的艺术

  • 单元测试是优质股,值得投资。但一些简单的属性访问方法或者价值不大的方法,是不值得花费时间进行测试的。
  • 人们不编写单元测试的很多借口都是因为代码中的设计缺陷。通常,抗议越强烈,就说明设计越糟糕。
  • 单元测试只有在达到一定测试覆盖率的时候,才能真正地发挥作用。你可以使用一些测试覆盖率工具,大致了解自己的单元测试的覆盖情况。
  • 不是测试越多质量就会越高,测试必须要有效。如果测试无法发现任何问题,也许它们就是没有测试对路。

20 先用它再实现它

“前进,先完成所有的库代码。后面会有大量时间看用户是如何思考的。现在只要把代码扔过墙去就可以了,我保证它没有问题。”很多成功的公司都是靠着“吃自己的狗食”活着。也就是说,如果要让你的产品尽可能地好,自己先要积极地使用它。幸运的是,我们不是在做狗食业务。但是,我们的业务是要创造出能调用的API和可以使用的接口。这就是说,你在说服其他人使用它之前,先得让自己切实地使用这些接口。事实上,在你刚做完设计但还没有完成后面的实现的时候,应该使用它。这个可行吗?

使用被称为TDD(Test Driven Development,测试驱动开发)的技术,你总是在有一个失败的单元测试后才开始编码。测试总是先编写。通常,测试失败要么是因为测试的方法不存在,要么是因为方法的逻辑还不足以让测试通过。

编程之前,先写测试 Write tests before writing code

先写测试,你就会站在代码用户的角度来思考,而不仅仅是一个单纯的实现者。这样做是有很大区别的,你会发现因为你自己要使用它们,所以能设计一个更有用、更一致的接口。除此之外,先写测试有助于消除过度复杂的设计,让你可以专注于真正需要完成的工作。

看看下面编程的例子,这是一个可以两人玩的“井字棋游戏”。开始,你会思考如何为这个游戏做代码设计。你也许会考虑需要这些类,例如:TicTacToeBoard、Cell、Row、Column、Player、User、Peg、Score和Rules。咱们从TicTacToeBoard类开始,它就代表了井字棋本身(从游戏的核心逻辑而不是UI角度说)。这可能是TicTacToeBoard类的第一个测试,是用C#在NUnit测试框架下编写的。它创造了一个游戏面板,用断言来检查游戏没有结束。

测试失败,因为类TicTacToeBoard还不存在,你会得到一个编译错误。如果它通过了,你一定很惊讶,不是吗?这也可能会发生,只是概率很小,但确实可能会发生。在测试通过之前,先要确保测试是失败的,目的是希望暴露出测试中潜在的bug。下面我们来实现这个类。

在属性GameOver中,我们现在只返回false。一般情况下,你会用必要的最少代码让测试通过。从某种角度上说,这就是在欺骗测试——你知道代码还没有完成。但是没有关系,后面的测试会迫使你再返回来,继续添加功能。下一步是什么呢?首先,你必须决定谁先开始走第一步棋,我们就要设第一个比赛者。

先为第一个比赛者写一个测试。这时,测试会迫使你做一个决定。在完成它之前,你必须决定如何在代码中表示比赛者,如何把它们分配到面板上。这里有一个主意。

这会告诉面板,游戏玩家Mark使用X。

这样做当然可以,你真的需要Player这个类,或者第一个玩家的名字吗?也许,稍后你需要知道谁是赢家。但现在它还不是问题。YANGI(1)(你可能永远都不需要它)原则说过,如果不是真正需要它的时候,你就不应该实现这个功能。基于这一点,现在还没有足够的理由表示你需要Player这个类。

别忘了,我们还没有实现TicTacToeBoard类中的SetFirstPlayer()方法,并且还没有写Player这个类。我们仍然是先写一个测试。我们假设下面的代码是用来设置第一个玩家的。

它表示设X为第一个玩家,比第一个版本要更简单。但是,这个版本隐藏着风险:你可以传任何字母给SetFirstPlayer()方法,这就意味着你必须添加代码来检查参数是O还是X,并且需要知道如果它不是这两个值的时候该如何处理。因此要更进一步简单化。我们有一个简单的标志来标明第一个玩家是O还是X。知道了这个,我们现在就可以写单元测试了。

我们可以将FirstPlayerPegIsX设为布尔类型的属性,并把它设为期望的值。这看起来挺简单的,也容易使用,比复杂的Player类容易很多。测试写好了,你就可以通过在TicTacToeBoard类中实现FirstPlayerPegIsX属性,让测试通过。

你看,我们是以Player类开始,最后却只使用了简单的布尔类型属性。这是如何做到的呢?这种简化就是在编写代码之前让测试优先实现的。但记住,我们不是要扔掉好的设计,就只用大量的布尔类型来编码所有的东西。这里的重点是:什么是成功地实现特定功能的最低成本。总之,程序员很容易走向另一个极端——一些不必要的过于复杂的事情——测试优先会帮助我们,防止我们走偏。

消除那些还没有编写的类,这会很容易地简化代码。相反,一旦你已经编写了代码,也许会强迫自己保留这些代码,并继续使用它(即使代码已经过期作废很久了)。

当你开发设计面向对象系统的时候,可能会迫使自己使用对象。有一种倾向认为,面向对象的系统应该由对象组成,我们迫使自己创建越来越多的对象类,不管它们是否真的需要。添加无用代码总是不好的想法。

好的设计并不意味着需要更多的类 Good design doesn’t mean more classes

TDD有机会让你编写代码之前(或者至少在深入到实现之前),可以深思熟虑将如何用它。这会迫使你去思考它的可用性和便利性,并让你的设计更加注重实效。当然,设计不是在开始编码的时候就结束了。你需要在它的生命周期中持续地添加测试,添加代码,并重新设计代码(更多信息参阅第113页习惯28)。先用它再实现它。将TDD作为设计工具,它会为你带来更简单更有实效的设计。

切身感受

这种感觉就是,只在有具体理由的时候才开始编码。你可以专注于设计接口,而不会被很多实现的细节干扰。

平衡的艺术

  • 不要把测试优先和提交代码之前的测试等同起来。测试先行可以帮助你改进设计,但是你还是需要在提交代码之前做测试。
  • 任何一个设计都可以被改进。
  • 你在验证一个想法或者设计一个原型的时候,单元测试也许并不适合。但是,万一这些代码不幸仓促演变成了一个真正的系统,就必须要为它们添加测试(但是最好能重新开始设计系统)。
  • 单纯的单元测试无法保证好的设计,但它们会对设计有帮助,会让设计更加简单。

21 不同环境,就有不同问题

“只要代码能在你的机器上运行就可以了,谁会去关心它是否可以在其他平台上工作。你又不用其他平台。”

如果厂商或者同事说了这样的套话:“哦,那不会有什么不同。”你可以打赌,他们错了。只要环境不同,就很可能会有不同的问题。Venkat真正在项目中学到了这一课。他的一个同事抱怨说,Venkat的代码失败了。但奇怪的是,问题在于,这与在Venkat机器上通过的一个测试用例一模一样。实际上,它在一台机器上可以工作,在另一台机器上就不工作。最后,他们终于找到了罪魁祸首:一个.NET环境下的API在Windows XP和Windows2003(2)上的行为不同。平台的不同,造成了结果的不一样。

他们算是幸运的,能够偶然发现这个问题。否则,很可能在产品投入使用的时候才会发现。如果很晚才发现这个问题,成本会非常昂贵——想象一下产品发布之后,才发现它并不支持应该支持的平台,那会怎么样。也许,你会要求测试团队在所有支持的平台上进行测试。如果他们是手工进行测试,可能并不是最可靠的测试办法。我们需要更加面向开发者的测试办法。

你已经编写了单元测试,测试你的代码。每次在修改或者重构代码的时候,在提交代码之前,你会运行测试用例。那么现在所要做的就是在各种支持的平台和环境中运行这些测试用例。

如果你的应用程序要在不同的操作系统上运行(例如MacOS、Linux、Windows等),或者一个操作系统的不同版本(例如Windows 2000、Windows XP、Windows 2003等),你需要测试所有的操作系统。如果你的应用程序要在不同版本的Java虚拟机或者不同的.NET CLR中运行,你也需要测试它们。

但是它在我的机器上可以工作

曾经有这样一个客户,他需要提高他们的OS/2系统性能。于是一个莽撞的开发人员打算用汇编从头开始重写OS/2的调度程序。从某种程度上说,事实上它是可以工作的。它在最初的开发人员的机器上工作得非常好,但是在其他人的机器上就不能用。他们甚至尝试了从同一个厂商那里购买硬件,安装相同版本的操作系统、数据库和其他的工具,但都徒劳无功。他们甚至尝试在每天的同一个时间,以同一个方向面朝机器,宰鸡向众神祭祀,希望能有好运(呵呵,这是我杜撰的,但其他都是真实的)。团队最终只好放弃了这个方案。与没有文档的内部操作系统纠缠在一起,绝对是非常脆弱的。这不是敏捷的做法。但是,也许你已经有时间压力了,因此,你怎么可能有时间在多个平台上运行测试呢?这就要靠持续集成(3)来拯救了。

使用自动化会节省时间 Automate to save time

我们在前面的保持可以发布中学过,用一个持续集成工具,周期性地从源代码控制系统中取得代码,并运行代码。如果有任何测试失败了,它会通知相关的开发者。通知方式可能是电子邮件、页面、RSS Feed,或者其他一些新颖的方式。

要在多个平台上测试,你只要为每个平台设置持续集成系统就行了。当你或者同事提交了代码,测试会在每个平台上自动运行。这样,提交代码之后的几分钟,你就可以知道它是否可以在不同的平台上运行!这是多么英明的办法呀!

构建机器的硬件成本相当于开发人员的几个小时而已。如果需要,你甚至可以使用像VMware 或Virtual PC这样的虚拟机产品,在一台机器上运行不同版本的操作系统、VM或CLR。不同环境,就有不同问题。使用持续集成工具,在每一种支持的平台和环境中运行单元测试。要积极地寻找问题,而不是等问题来找你。

切身感受

感觉就像是在做单元测试,非但如此,而且还是跨越不同的世界的单元测试。

平衡的艺术

  • 硬件比开发人员的时间便宜。但如果你有很多配置,要支持大量的平台,可以选择哪些平台需要内部测试。
  • 只因为不同的栈层顺序、不同的单词大小写等,就能发现很多平台上的bug。因此,即使运行用Solaris的客户比用Linux的少很多,你仍然要在两个系统上都进行测试。
  • 你不希望因为一个错误而收到5次通知轰炸(这就像是双重征税,会导致电子邮件疲劳症)。可以设置一个主构建平台或者配置,降低其他构建服务器的运行频率,这样在它失败的时候,你就有足够的时间来修复主构建平台。或者汇总所有错误报告信息到一个地方,进行统一处理。

22 自动验收测试

“很好,你现在用单元测试来验证代码是否完成了你期望的行为。发给客户吧。我们很快会知道这是否是用户期望的功能。”

你与用户一起工作,开发他们想要的功能。但现在,你要能确保他们得到的数据是正确的,至少在用户看来它是正确的。

几年前,Andy做了一个项目。在项目中,他们的行业标准规定凌晨12:00点是一天的最后一分钟,12:01是一天最早一分钟(一般情况下,商业计算机系统认为凌晨11:59是一天的最后一分钟,12:00是一天最早一分钟)。在验收测试的时候,这个很小的细节导致一个严重的问题——无法进行正确的合计。关键业务逻辑必须要独立进行严格的测试,并且最后需要通过用户的审批。但你也不可能拉着用户,逐一检查每个单元测试的运行结果。实际上,你需要能自动比较用户期望和实际完成的工作。有一个办法可以使验收测试不同于单元测试。你应该让用户在不必学习编码的情况下,根据自己的需要进行添加、更新和修改数据。你有很多方法来实现它。

Andy使用了一些架构,把测试数据放到一个平面文件中,并且用户可以直接修改这些数据。Venkat使用Excel做过类似的事情。根据环境的不同,也可以找出一种能让用户自然接收的方法(数据可以在平面文件、Excel文件、数据库中)。或者可以考虑选择一个现成的测试工具,它们会为你完成很多功能。FIT(4),即集成测试框架,它很实用,可以更容易地使用HTML表格定义测试用例,并比较测试结果数据。

获取验收数据

一个客户以前使用过Excel开发的定价模型。我们就通过写测试,比较应用的价格输出结果是否与Excel的一致,然后,必要的话,纠正应用中的逻辑和公式。这样用户可以简单地修改验收测试标准,定价相关的关键业务逻辑是正确的,每个人对项目都很有信心。

使用FIT,客户可以定义带有新功能的使用样本。客户、测试人员和开发人员(根据样本)都可以创建表格,为代码描述可能的输入和输出值。开发人员会参照带有正开发的代码结果的FIT表格中的样本编写测试代码。测试结果成功或者失败,都会显示在HTML页面中,用户可以很方便地查阅。

如果领域专家提供了业务的算法、运算或者方程式,为他们实现一套可以独立运行的测试(参见第136页习惯35)。要让这些测试都成为测试套件的一部分,你会在项目生命周期中确保持续为它们提供正确的答案。

为核心的业务逻辑创建测试。让你的客户单独验证这些测试,要让它们像一般的测试一样可以自动运行。

切身感受

它像是协作完成的单元测试:你仍然是在编写测试,但从其他人那里获得答案。

平衡的艺术

  • 不是所有客户都能给你提供正确的数据。如果他们已经有了正确的数据,就根本不需要新系统了。
  • 你也许会在旧系统(也许是电脑系统,也许是人工系统)中发现以前根本不知道的bug,或者以前不存在的真正问题。
  • 使用客户的业务逻辑,但是不要陷入无边无际的文档写作之中。

23 度量真实的进度

“用自己的时间表报告工作进度。我们会用它做项目计划。不用管那些实际的工作时间,每周填满40小时就可以了。”

时间的消逝(通常很快)可以证明:判断工作进度最好是看实际花费的时间而不是估计的时间。哦,你说早已经用时间表进行了追踪。不幸的是,几乎所有公司的时间表都是为工资会计准备的,不是用来度量软件项目的开发进度的。例如,如果你工作了60个小时,也许你的老板会让你在时间表上只填写40个小时,这是公司会计想看到的。所以,时间表很难真实地反映工作完成状况,因此它不可以用来进行项目计划、评估或表现评估。

即使没有时间表,一些开发人员还是很难面对现实了解自己的真实进度。你曾经听到开发人员报告一个任务完成了80%吗?然而过了一天又一天,一周又一周,那个任务仍然是完成了80%?随意用一个比率进行度量是没有意义的,这就好比是说80%是对的(除非你是政客,否则对和错应该是布尔条件)。所以,我们不应该去计算工作量完成的百分比,而应该测定还剩下多少工作量没有完成。如果你最初估计这个任务需要40个小时,在开发了35个小时之后,你认为还需要另外30个小时的工作。那就得到了很重要的度量结果(这里诚实非常重要,隐瞒真相毫无意义)。

专注于你的方向 Focus on where you’re going

在你最后真正完成一项任务时,要清楚知道完成这个任务真正花费的时间。奇怪的是,它花费的时间很可能要比最初估计时间长。没有关系,我们希望这能作为下一次的参考。在为下一个任务估计工作量时,可以根据这次经验调整评估。如果你低估了一个任务,评估是2天,它最后花费了6天,那么系数就是3。除非是异常情况,否则你应该对下次估计乘以系数3。你的评估会波动一段时间,有时候过低估计,有时候过高估计。但随着时间的推移,你的评估会与事实接近,你也会对任务所花费的时间有更清楚的认识。

我的小姨子曾经在某个大型国际咨询公司中工作。每天每隔6分钟她们就得登记她们的时间。她们甚至有代码来专门记录填表登记时间所花费的时间。这个代码不是0、9999或者一些容易记的代码,而是类似948247401299-44b这么一个临时的代码。这就是为什么你不愿意把会计部门的规则和约束掺合到项目中的原因。

如果能一直让下一步工作是可见的,会有助于进度度量。最好的做法就是使用待办事项(backlog)。

待办事项就是等待完成的任务列表。当一个任务被完成了,它就会从列表中移除(逻辑上的,而物理上就是把它从列表中划掉,或者标识它是完成的状态)。当添加新任务的时候,先排列它们的优先级,然后加入到待办事项中。你也可以有个人的待办事项、当前迭代的待办事项或者整个项目的待办事项。(5)

通过代办事项,就可以随时知道下一步最重要的任务是什么。同时,你的评估技巧也在不停地改进,你也会越来越清楚完成一项任务要花费的时间。

清楚项目的真实进度,是一项强大的技术。度量剩下的工作量。不要用不恰当的度量来欺骗自己或者团队。要评估那些需要完成的待办事项。

Scrum方法中的sprint 在Scrum开发方法中(Sch04),每个迭代被称作sprint,通常为30天时间。sprint的待办事项列表是当前迭代任务列表,它会评估剩下的工作量,显示每个任务还需要多少小时可以完成。

每个工作日,每个团队成员会重新评估完成一个任务还需要多少小时。不管怎么样,只要所有任务的评估总和超过了一个迭代剩余的时间,那么任务就必须移到下一个迭代中开发。

如果每月还有一些剩余的时间,你还可以添加新的任务。这样做,客户一定会非常喜欢。

切身感受

你会觉得很舒服,因为你很清楚哪些任务已经完成,哪些是没有完成的,以及它们的优先级。

平衡的艺术

  • 6分钟作为一个时间单位,它的粒度实在太细了,这不是敏捷的做法。
  • 一周或者一个月的时间单元,它的粒度太粗了,这也不是敏捷的做法。
  • 关注功能,而不是日程表。
  • 如果你在一个项目中花费了很多时间来了解你所花费的时间,而没有足够的时间进行工作,那么你在了解你所花费的时间上花费的时间就太多了。听懂了吗?
  • 一周工作40个小时,不是说你就有40个小时的编码时间。你需要减去会议、电话、电子邮件以及其他相关活动的时间。

24 倾听用户的声音

“用户就是会抱怨。这不是你的过错,是用户太愚蠢了,连使用手册都看不懂。它不是一个bug,只是用户不明白如何使用而已。他们本应该知道更多。”

Andy曾经在一家大公司工作过,为高端的Unix工作站开发产品。在这个环境中,你不是简单地运行setup.exe文件或者pkgadd命令,就可以完成软件的安装。你必须在工作站上复制文件并调整各种设置。

Andy和他的团队成员们觉得一切都工作得很顺利。直到一天,Andy走过技术支持部门的工作间,听到一个技术支持工程师对着电话大笑:“哦,这不是bug,你只是犯了一个每个人都会犯的错误。”并且,不只是这一个工程师,整个部门都在嘲笑这些可怜、天真和愚蠢的客户。

倒霉的客户必须要配置那些包含了一些魔术数字的模糊系统文件,否则系统根本不会运行。系统既没有错误提示消息,也不会崩溃,只是显示大黑屏和一个斗大的“退出”按钮。事实上,安装说明书中有一行提到了这样的问题,但显然80%的用户忽略了这个信息,因此只能求助公司的技术支持部门,并遭到他们的嘲笑。

这是一个bug It is a bug

正如我们在第128页第7章中所说,当出了错误,你要尽可能地提供详细信息。黑屏和含义不明的“退出”按钮是很不友好的行为。更糟糕的是,在得到用户反馈的时候,还嘲笑用户愚蠢,而不去真正地解决问题。

不管它是否是产品的bug,还是文档的bug,或者是对用户社区理解的bug,它都是团队的问题,而不是用户的问题。

下面一个案例是:一个昂贵的专业车间的控制系统,没有任何一个用户会使用。因为,使用系统的第一步是要输入用户名和密码,进行登录。但这个车间的大部分工人都是文盲,没有人去问过他们,也没有去收集他们的反馈。就这样,为用户安装了一个无用的系统。最后,花费巨大的费用,开发人员重新开发了一个基于居左图片的使用界面。

我们花费了很大的精力从单元测试之类的代码中获得反馈,但却容易忽略最终用户的反馈。你不仅需要和真实用户(不是他们的经理,也不是业务分析师之类的代理人)进行交谈,还需要耐心地倾听。即使他们说的内容很傻!每一个抱怨的背后都隐藏了一个事实。找出真相,修复真正的问题。

切身感受

对客户的那些愚蠢抱怨,你既不会生气,也不会轻视。你会查看一下,找出背后真正的问题。

平衡的艺术

  • 没有愚蠢的用户。
  • 只有愚蠢、自大的开发人员。
  • “它就是这样的。”这不是一个好的答案。
  • 如果代码问题解决不了,也许可以考虑通过修改文档或者培训来弥补。
  • 你的用户有可能会阅读所有的文档,记住其中的所有内容。但也可能不会。

【注释】

  1. Ron Jeffries创造的词,它是You Aren’t Gonna Need It的缩写。
  2. 参见.NET Gotchas中的Gotcha #74[Sub05]。
  3. 阅读Martin Fowler 写的一篇重要的文章Continuous Integration,http://www.martinfowler.com/articles/continuousIntegration.html。
  4. http://fit.c2.com。
  5. 使用待办事项及个人与项目管理工具的列表的更多信息,参考Ship It ![RG03]。