【IT168 技术文章】
什么是用户例事(user story)
假定这个项目的客户是一个饮料自动售货机的制造商。他们要求为其售货机开发一款软件。我们可以找他们的市场经理了解这个软件的需求。因此,我们的客户就是他们的市场经理。谈需求的时候,他这样说:“用户往售货机里每塞一个硬币,售货机都要显示当前该客户已经投了多少钱。当用户投的钱够买某一款饮料时,代表这款饮料的按钮的灯就会亮。如果该用户按了这个按钮,售货机就放一罐饮料到出口,然后找零钱给他。”
上面描述的是一件事情,一件用户通过系统完成其一个有价值的目标(买一罐饮料)的事。这样的过程就叫“用户案例(user case)”或者“用户例事(user story)”。也就是说,上面客户所说的话就是在描述一个用户例事(user story)。为什么用例事这个词,是指在一个系统前,每个用户要完成同样的目标,都做这个系统设定的例行的事。这件事情不是一个例子,所以不叫事例;也不是故事,而是一个例行的事。
如果我们想要记下这段用户例事,我们可能会用这样的格式:
名称:卖饮料
事件:
1. 用户投入一些钱。
2. 售货机显示用户已经投了多少钱。
3. 如果投入的钱足够买某种饮料,这种饮料对应的按钮的灯就会亮。
4. 用户按了某个亮的按钮。
5. 售货机卖出一罐饮料给他。
6. 售货机找零钱给他。
注意到,一个用户例事里面的事件可以这样描述:
1. 用户做XX。
2. 系统做YY。
3. 用户做ZZ。
4. 系统做TT。
5. ...
用户例事只是描述系统的外在行为
一个用户例事只是以客户能够明白的方式,描述了一个系统的外在行为。它完全忽略了系统的内部动作,比如,下面有下划线的那些文字就属于不应该出现在用户例事中的系统内部动作。
1. 用户投入一些钱。
2. 售货机将塞进来的钱存在钱箱里,然后发送一条命令给屏幕,屏幕显示目前已经投入的金额。
3. 售货机查询数据库里面所有饮料的价格,判定钱足够买哪些饮料,对于钱足够买的那些饮料,对应的按钮的灯就会亮。
4. 用户按下一个亮的按钮。
5. 售货机卖出一罐饮料给用户,然后将数据库里面该饮料的存货数量减1。
6. 售货机找零钱给用户。
不管是口头描述,还是书面形式,这样的内容是描述用户例事时一个很常见的错误。特别的,千万不要提及任何有关数据库、记录和字段之类对客户一点意义都没有的东西。
评估发布时间
用户例事是用来干嘛的?假定客户希望在50天内提交这个系统,我们能做到吗?为了解答这个问题,我们就要在项目开始的阶段,试着找出所有的用户例事,然后评估一下,每一项历程需要多长的开发时间。怎么评估呢?
比如,我们现在收集了下面这些用户例事:
卖饮料:如上面所说的。
取消购买:在投入了一些钱后,用户可以取消购买。
输入管理密码:授权的人可以输入管理密码,然后增加存货、设定价格、拿走里面的钱等。
补充饮料:授权的人可以在输入管理密码后增加存货。
取出钱箱里的钱:授权的人在输入管理密码后,可以取出钱箱里的钱。
安全警报:有些事情经常发生的话,系统会自动打开安全警报。
打印月销售报表:授权的人可以打印出月销售报表。
打印月销售报表:授权的人可以打印出月销售报表。
然后,找出里面最简单的用户例事,这里的简单是指实现周期最短。我们不一定非常精准地判断哪个最简单,只要挑出你觉得最简单的就行。比如,我们觉得“输入管理密码”是最简单的用户例事。接下来,我们判断,该用户例事算1个“例事点(story point)”。
用户例事 例事点
卖饮料
取消购买
输入管理密码 1
补充饮料
取出钱箱里的钱
安全警报
打印月销售报表
不过,敏捷开发中,一般我们不会列出清单,而是做出一堆卡片贴在墙上,每张卡片记录一个用户例事,并将例事点写在卡片上面,这样的一张卡片就叫“例事卡(story card)”(如图1)。

图1
下面开始考虑其他用户例事。比如,对于“取出钱箱里的钱”这个例事,我们认为它跟“输入管理密码”这个例事一样简单,所以它应该也算1个例事点,我们在列表里面标上。当然,实际操作时,我们是在“取出钱箱里的钱”的例事卡上填上例事点。
对于“取消购买”,我们认为它应该是“取出钱箱里的钱”两倍的工作量,所以它算2个例事点。
对于“卖饮料”,我们认为它应该是“取消购买”两倍的复杂度,所以它应该算4个例事点。
对于“补充饮料”,我们认为它比“取出钱箱里的钱”复杂,但又比“卖饮料”简单,然后它又应该比“取消购买(2个例事点)”复杂,所以它应该是3个例事点。
类似的,我们认为“安全警报”应该比“补充饮料”简单一些,所以应该是2个例事点。
“打印月销售报表”应该与“卖饮料”一样复杂,所以应该算4个例事点。
这样的话,我们总共有17个例事点如下:
用户例事 例事点
卖饮料 4
取消购买 2
输入管理密码 1
补充饮料 3
取出钱箱里的钱 1
安全警报 2
打印月销售报表 4
总计 17
现在挑出任意一个用户例事,估计一下它要花(一个开发人员)多少时间来完成。假设我们之前做过跟“取出钱箱里的钱”类似的功能,所以我们就挑这个来计算,估计它要花5天的时间。也就是说,一个例事点要花5天的时间完成。现在我们有17个例事点,我们需要17×5=85天来完成这个项目,如果只是一个开发人员来做的话。假设现在我们的团队里有两个开发人员,则我们需要85÷2=43天来完成。
从目前的估计看,我们可以在50天的期限里做完这个项目。但现在得出结论还太早了。这样的评估,更多的是猜测。通常开发人员在工作量上的估算都很差。事实上,人们经常会低估了工作量。那我们应该怎么估计得更准呢?
实际的开发速度是多少
为了做到更准确的估计,我们让客户先给我们两周时间做一些实际的开发,以此测量一下我们在这两周里可以做多少的用户例事。我们叫这两周的时间为“迭代周期”。
哪些用户例事应该放在第一个迭代周期里面做?这可能完全是客户决定的,也可能是大家讨论以后决定的。不过挑出的这些用户例事的例事点不应该超过两周的承受能力。因为一个迭代周期有10 天(假设我们周末不工作),然后我们估计一个开发人员5天可以完成一个例事点。现在我们有两个开发人员,所以我们应该可以在这个迭代周期中完成(10÷5)×2=4个例事点。客户可以在总例事点不超过4的前提下,挑出一些用户例事在这个迭代周期里实现。他们可能会尽量挑选觉得最重要的例事。比如,对他们来说,销售报表和找零钱最重要,所以他们就挑出这两个用户例事。
假设两周的迭代周期过去,我们完成了“取出钱箱里的钱”,不过“打印月销售报表”没有完成,还剩0.5个例事点没有完成。也就是说,我们在这个迭代周期内完成了3-0.5=2.5个例事点(比原来的预计要差一些)。2.5个例事点这样的数值就是我们现在的参考值,即该团队每个迭代周期可以完成2.5个例事点。这个参考值对我们很有用。它有两个用处:
首先,我们可以假定在下个迭代周期也可以完成2.5个例事点,然后客户选择的用户例事的例事点总和不能超过2.5。
其次,在从第一个迭代周期取得参考值以后,我们可以重新估算我们的发布时间。本来我们估算每个开发人员完成1个例事点的时间是5天,现在有了实践数据,我们的团队(两个开发人员)可以在一个迭代周期(10 天)内完成2.5个例事点。总的例事点是17,计算一下,我们需要17÷2.5=7个迭代周期来完成,即14周,70天。也就是说,我们满足不了客户提出的期限(50天内)。怎么办?
预计不能如期完成时怎么办
很明显,现在我们完成不了全部的用户例事。在这50天里面,我们只能完成50÷10×2.5=12.5个用户例事。而现在有17个例事点,我们应该让客户挑出总计4.5个例事点的用户例事,推迟到下一个发布周期去。客户应该选择那些比较次要的用户例事。比如,客户可以推迟“打印月销售报表”这个用户例事。这只是开发不能如期完成时的解决方法之一,这种方法应该是在客户比较有诚意合作的前提下使用。
用户例事 例事点
卖饮料 4
取消购买 2
输入管理密码 1
补充饮料 3
取出钱箱里的钱 1
安全警报 2
总计 13
然后他还要挑出总和至少为0.5个例事点的例事。
但是,如果“月销售报表”这个用户例事对客户来说也是非常重要,而且其他的例事也不能推迟,这时怎么办?这时,我们就要试着简化这些用户例事。比如,原来“月销售报表”是要用一个第三方报表库来实现,而且还要画出饼状统计图。如果我们只是生成简单的文本格式的报表(这一格式应该可以被Excel导入,以便日后的处理),那么这个用户例事的例事点就会从4减少到2,节省2个例事点。如果客户同意的话,我们就可以将“打印月销售报表”分成两个用户例事“生成月销售文本报表”和“生成图形报表”,然后将后者推迟到下一个发布。
还有其他2.5个例事点需要推迟。我们可以简化“卖饮料”。假设本来我们可以卖不同价格不同类别的饮料。如果现在我们只是简单支持一种价格一种类别的饮料,这个用户例事的例事点可以从4减到2。在客户同意的情况下,我们就可以将“卖饮料”分割为“卖单一饮料”和“卖多种饮料”,然后将后者推迟到下个周期发布。
现在还剩0.5个例事点。我们再考虑一下“安全警报”。假设本来这个例事是要同时触发本机上的警报,和通知附近一个警察局的。如果现在我们只是触发本机警报,那所花的例事点就可以从2减到1。于是在客户同意的情况下,我们将“安全警报”分割为“本机安全警报”和“通知警察”,然后将后者推迟到下个发布。
现在新的例事卡片出来了:
用户例事 例事点
卖单一饮料 2
取消购买 2
输入管理密码 1
补充饮料 3
取出钱箱里的钱 1
本机安全警报 1
打印月销售文本报表 2
总计 12
现在我们总计有12个例事点要做(<=12.5)。上面这个筛选在本次发布的用户例事的过程,叫“发布计划编制”。
增加开发人员来满足发布期限
在上面的例子中,我们以推迟部分用户例事到下个发布周期的办法来解决问题。这种“控制开发范围”通常是最好的解决办法。不过,这种解决办法实施不了的情况下,那就只好保留所有的用户例事,然后增加更多的开发人员了。在这个例子中,假定我们需要n个开发人员才能在50天内完成17个例事点。50÷10×2.5×n÷2,n=2.7,我们需要3个开发人员,也就是多加一个开发人员进来。不过注意,团队人数加倍并不等于开发周期的减半,它可能只会缩短1/3。如果团队超过10个人的话,增加更多的人员可能反而会延缓项目的进度。而且项目开发周期越长,团队内的成员对整个项目代码的熟悉度就越少,加上不确定的人员流动,新来人员的业务不熟等其他可能性,这项目会越来越复杂。
总的意思就是,项目人数不能太多,周期不能太长。
根据参考值来掌控项目
每个迭代周期2.5个例事点的这个参考值只是第一个迭代周期的数据,第二个迭代周期可能会变成2或者3(一般不会变动得太大)。假设是2的情况下,对于第三个迭代周期,我们就要将参考值设为2,然后让客户以2为例事点总数来挑选用户例事。
对于大多数项目,参考值很快就会稳定下来,比如在几个迭代周期后。当这个值稳定下来后,我们就要重新估计开发周期,重新进行“发布计划编制”。如果这个参考值告诉我们,我们每个迭代周期可以做3个例事点的话,我们就要让客户挑选更多的用户例事放在这次的发布计划中。相反如果这个参考值是2的话,我们就要让客户减少用户例事(需要的话可以分割一些用户例事),如果团队人员还不多的话,可以增加更多的开发人员。
这是项目的初始阶段绝对要注意的。
发布计划编制
发布计划编制,估算每个用户例事时要考虑哪些细节,忽略哪些细节呢?
在项目初始,我们要找出这个发布周期内所有主要的用户例事,评估每个例事的例事点。可是要如何评估其中的细节呢?比如对于“卖饮料”这个简单的标题,省略了很多的细节,比如用户会投入什么样的钱?纸币可以吗?人民币可以吗?按钮的灯的亮度要多少?可不可以多个按钮对应一种饮料?按钮被按下以后,要不要变暗?找零钱是不是全部找10分的面额?
我们是不是要考虑上面所有的细节?比如,对于按钮灯的亮度,我们就不用考虑,它对我们的工作量没影响。不过,零钱的面额就对我们的工作量很有影响,我们要认真考虑一下。处理不同币种也要考虑。
一般情况,我们不用太担心会漏过什么细节。对于每个用户例事,只要考虑一些“重要”问题就行。当然,这里的“重要”就要根据经验及客户的观点来决定了。
如果我们觉得,这个用户例事不好估算,那可能的原因就是:
1. 这个用户例事太大。这种情况我们可以将该用户例事分割出若干个新的用户例事,比如将“卖饮料”分割出:
(1)显示总投入金额。
(2)金额够买的饮料对应的按钮灯亮起来。
(3)按下亮灯的按钮,可以买到对应的饮料。
2. 我们之前从没开发过自动售货机的程序,因此,我们不知道开发这样的程序有多复杂。这样的话,我们就要做一些实验,比如做一个让售货机找钱的小程序。这种试验就叫做“spike”。
迭代周期内的计划编制
对于这个迭代周期内选择的所有用户例事,不像在发布计划编制那样,只是考虑一些重要的细节。现在我们要从客户那里调查到所有的细节,比如对于“卖饮料”,我们可能会在白板上画出用户交互的草图,然后跟客户一起讨论:
这是一台自动售货机……
用户投入硬币……
假设他投入的是50分,而价格是40分,那么按钮就会亮起
用户按一个亮起的按钮,一罐饮料会掉到售货机的出口
找零钱……
在跟客户详细讨论,了解足够的细节以后,我们才发现,事实上这个用户例事“卖饮料(只卖单一的)”的例事点远远比我们预计的要麻烦,这时候应该类似前面的发布计划编制那样,分割出小的用户例事,挑出一些放在下一个迭代周期内;或者挑出这个迭代周期内的一些用户例事放在下一个迭代周期。反之,如果发现这个用户例事比我们想像的要简单,我们就要增加更多的用户例事到这个迭代周期内。
用户例事不是全部
用户例事只是跟用户交流的开始,而不是全部。假设现在已经从客户那里得到足够详细的需求,我们可以开始实现了。注意,我们不用把所有用户提供的细节都记录下来。为什么呢?假设以后你有些忘记用户例事,而客户又在你旁边,你是直接问客户,还是去看需求文档?当然是直接问客户。客户可以提供更准确、更完整的需求给你。
特别要注意的是,只要你一完成一个用户例事,你就要让客户看一下,或者实际操作一下。因为客户对已经做的东西了解得越多,他就可以提供越准确、越完整的需求。
用户挑选完用户例事以后,在之后的两个星期内,我们就要将这些用户例事逐个完成。每个用户例事我们都会设计结构、编码、测试等。每做完一个用户例事,我们都要让客户验证一下系统是不是他所想的那样。
在这两个星期内,如果我们提早完成用户例事,我们就要让客户挑更多的用户例事。相反的,如果我们不能及时完成,我们就要让客户知道当前的进度。