2015-05-13 21:16:50 josonchen 阅读数 536
  • SCRUM敏捷开发视频教程

    SCRUM敏捷开发视频教程,该课程为你分享SCRUM敏捷开发,理解敏捷的本质,认识中国IT行业对敏捷的挑战,学会让敏捷落地的实用招数。 嘉宾介绍:张传波 1. 创新工场创业课程(敏捷课程)讲师 2.软件研发管理佳实践顾问(曾任华为某团队研发顾问) 3. 中国敏捷联盟《ADBOK》(敏捷开发知识体系)项目组成员 二十年软件开发、软件设计、需求分析、项目管理、部门管理、公司管理及过程改进等经验,亲历“无数”项目,涉猎建筑、通讯、互联网、电力、金融、制造业、政府等领域,熟悉软件生命周期的全部过程

    10408 人正在学习 去看看 CSDN讲师
一件用户通过系统完成他一个有价值的目标(买一罐饮料)的事。这样的过程就叫“用户案例(user case)”或者“用户故事(user story)”。本文描述了敏捷开发的技巧:如何以用户故事管理项目.

敏捷开发技巧:以用户故事管理项目

Wingel 发表于 2007-02-12 08:08:13
作者:Wingel     来源:Wingel' blog
摘自:Matrix
 
                                                
什么是用户故事(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
补充饮料        
取出钱箱里的钱        
安全警报        
打印月销售报表        

不过一般我们不会列出清单,而是做出一堆卡片贴在墙上,每张卡片记录一个用户故事,然后将故事点写在卡片上面:

image

这样的一张卡片就叫“故事卡(story card)”。

然后开始考虑其他用户故事。比如,对于“取出钱箱里的钱”这个故事,我们认为它跟“输入管理密码”这个故事一样简单,所以它应该也是算1个故事点。我们在列表里面标上。当然,实际操作的时候,我们是在“取出钱箱里的钱”的故事卡上填上故事点。
用户故事            故事点
卖饮料        
取消购买        
输入管理密码     1
补充饮料        
取出钱箱里的钱   1
安全警报        
打印月销售报表        

对于“取消购买”,我们认为它应该是“取出钱箱里的钱”的两倍的工作量,所以它算2个故事点。
用户故事           故事点
卖饮料        
取消购买             2
输入管理密码    1
补充饮料        
取出钱箱里的钱  1
安全警报        
打印月销售报表        

对于“卖饮料”,我们认为它应该是“取消购买”两倍的复杂度,所以它应该算4个故事点。
用户故事           故事点
卖饮料            4
取消购买            2
输入管理密码   1
补充饮料        
取出钱箱里的钱 1
安全警报        
打印月销售报表        

对于“补充饮料”,我们又认为它比“取出钱箱里的钱”复杂,但又比“卖饮料”简单,然后它又应该比“取消购买(2个故事点)”复杂,所以我们认为它应该是3个故事点:
用户故事          故事点
卖饮料            4
取消购买            2
输入管理密码   1
补充饮料            3
取出钱箱里的钱 1
安全警报        
打印月销售报表
        
类似的,我们认为“安全警报”应该比“补充饮料”简单一些,所以应该是2个故事点:
用户故事        故事点
卖饮料        4
取消购买        2
输入管理密码   1
补充饮料        3
取出钱箱里的钱        1
安全警报        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的前提下,挑出一些用户故事在这个迭代周期里实现。他们可能会尽量挑选他们觉得最重要的故事。比如,对他们来说,销售报表跟找零钱最重要,所以他们就挑出这两个:
用户故事        故事点
取出钱箱里的钱        1
打印月销售报表        2
总计        3

假设两周的迭代周期过去了,我们完成了“取出钱箱里的钱”,不过“打印月销售报表”没有完成,还剩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个故事点。如果客户同意的话,我们就可以将“打印月销售报表”分成两个用户故事“生成月销售文本报表”和“生成图形报表”,然后将后者推迟到下一个发布。

现在新的故事卡片出来了:
用户故事        故事点
卖饮料        4
取消购买        2
输入管理密码     1
补充饮料        3
取出钱箱里的钱        1
安全警报        2
打印月销售文本报表        2
总计        15

还有其他的2.5个故事点要推迟,我们可以简化“卖饮料”。假设本来我们可以卖不同价格不同类别的饮料。如果现在我们只是简单支持一种价格一种类别的饮料的话,那这个用户故事的故事点可以从4减到2了。客户如果同意的话,我们就可以将“卖饮料”分割为“卖单一饮料”和“卖多种饮料”,然后将后者推迟到下个周期发布:
用户故事        故事点
卖单一饮料        2
取消购买        2
输入管理密码    1
补充饮料        3
取出钱箱里的钱        1
安全警报        2
打印月销售文本报表        2
总计        13

现在还剩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分的面额?
我们是不是要考虑上面所有的细节?对于按钮灯的亮度,我们就不用考虑了,它对我们的工作量没影响。不过,零钱的面额就对我们的工作量很有影响,我们要认真考虑一下(找一堆10分的零钱就很容易实现;如果要尽量减少零钱的个数就比较麻烦了)。处理不同币种也要考虑。
    一般情况,我们不用太担心会漏过什么细节。对于每个用户故事,只要考虑一些“重要”问题就行了。当然,这里面的“重要”,就要根据经验以及客户的观点来决定了。

如果我们不好估算的话怎么做

如果我们觉得,这个用户故事不好估算,那可能的原因就是:

1.  这个用户故事太大。这种情况我们就可以将这个用户故事分割出若干个新的用户故事,比如:
将“卖饮料”分割出:
1:显示总投入金额。
2:金额够买的饮料对应的按钮灯亮起来。
3:按下亮灯的按钮,可以买到对应的饮料。
2.  我们之前从没开发过自动售货机的程序。因此,我们不知道开发这样的程序有多复杂。这样的话,我们就要做一些实验了,比如做一个让售货机找钱的小程序。这种试验就叫“spike”(翻译不出来)。

迭代周期内的计划编制

对于这个迭代周期内选择的所有用户故事,不像在发布计划编制那样,只是考虑一些重要的细节,现在我们要从客户那里调查到所有的细节。比如对于“卖饮料”,我们可能会在白板上画出用户交互的草图,然后跟客户一起讨论:
这是一台自动售货机……
用户投入硬币……
假设他投入的是50分,而价格是40分,那么按钮就会亮起(别忘了我们现在做的只卖单一饮料)
用户按一个亮起的按钮,一罐饮料会掉到售货机的出口
找零钱……

在跟客户详细讨论完,了解了足够的细节以后,我们才发现,事实上这个用户故事“卖饮料(只卖单一的)”的故事点远远比我们预计的要麻烦,这时候应该类似前面的发布计划编制那样,1、分割出小的用户故事,挑出一些放在下一个迭代周期内;或者2、挑出这个迭代周期内的一些用户故事放在下一个迭代周期。反之,如果发现这个用户故事比我们想像的还要简单,那我们就要增加更多的用户故事到这个迭代周期内。

用户故事只是跟用户交流的开始,而不是全部

假想现在已经从客户那边得到足够详细的需求了,我们可以开始实现了。注意,我们不用把所有用户提供的细节都记录下来。为什么呢?假设以后,你有点忘记用户故事,而客户又在你旁边,你是直接问客户,还是去找看需求文档找到你要的东西?当然是直接问客户了。客户可以提示更准确,更完整的需求给你。特别要注意的是,以后只要你一完成一个用户故事,你就要让客户看一下,或者实际的操作一下,因为客户对已经做的的东西了解得越多,那他就可以提供越准确越完整的需求。
用户挑选完用户故事以后,在之后的两个星期内,我们就要将这些用户故事逐个完成。每个用户故事我们都会设计结构,编码,测试等等。每做完一个用户故事,我们都要让客户验证一下系统是不是他所想的那样。

在这两个星期内,如果我们提早完成了用户故事,我们就要让客户挑更多的用户故事。相反的,如果我们不能及时完成,我们就要让客户知道当前的进度。

总结
  我觉得这章的内容跟其他的软件工程书一样,看看,参考参考,具体的情况还是要具体的分析,不过这里面的用户故事(user story)跟故事卡片的概念就很不错,可以引用。

里面有一些图表可能显示不清,惯例,pdf 完整版:


敏捷开发的必要技巧
2015-05-13 21:15:57 josonchen 阅读数 362
  • SCRUM敏捷开发视频教程

    SCRUM敏捷开发视频教程,该课程为你分享SCRUM敏捷开发,理解敏捷的本质,认识中国IT行业对敏捷的挑战,学会让敏捷落地的实用招数。 嘉宾介绍:张传波 1. 创新工场创业课程(敏捷课程)讲师 2.软件研发管理佳实践顾问(曾任华为某团队研发顾问) 3. 中国敏捷联盟《ADBOK》(敏捷开发知识体系)项目组成员 二十年软件开发、软件设计、需求分析、项目管理、部门管理、公司管理及过程改进等经验,亲历“无数”项目,涉猎建筑、通讯、互联网、电力、金融、制造业、政府等领域,熟悉软件生命周期的全部过程

    10408 人正在学习 去看看 CSDN讲师
本文通过简单通俗的例子, 告诉我们如何判断代码的稳定性和代码中的异类, 并且如何重构此类代码.

敏捷开发技巧-消除代码异味

Wingel 发表于 2006-12-13 08:41:41
作者:Wingel     来源:Wingel's blog

摘要:

本文通过简单通俗的例子, 告诉我们如何判断代码的稳定性和代码中的异类, 并且如何重构此类代码.


 

异味这个词,可能有点抽象,我们先看一下下面的例子

这是一个CAD系统. 现在,它已经可以画三种形状了:线条,长方形,跟圆.先认真的看一下下面的代码:

class Shape {                                                                                   
       final static int TYPELINE = 0;                                                              
       final static int TYPERECTANGLE = 1;                                                          
       final static int TYPECIRCLE = 2;                                                            
       int shapeType;                                                                              
       //线条的开始点
     //长方形左下角的点
     //圆心
     Point p1;                                                                                    
       //线条的结束点
     //长方形的右上角的点
     //如果是圆的话,这个属性不用
     Point p2;                                                                                    
       int radius;                                                                                  
    }                                                                                              
class CADApp {                                                                                  
       void drawShapes(Graphics graphics, Shape shapes[]) {                                        
           for (int i = 0; i < shapes.length; i++) {                                                
               switch (shapes[i].getType()) {                                                      
                  case Shape.TYPELINE:                                                              
                      graphics.drawLine(shapes[i].getP1(), shapes[i].getP2());                      
                      break;                                                                        
                  case Shape.TYPERECTANGLE:                                                        
                      //画四条边
               graphics.drawLine(...);                                                      
                      graphics.drawLine(...);                                                      
                      graphics.drawLine(...);                                                      
                      graphics.drawLine(...);                                                      
                      break;                                                                        
                  case Shape.TYPECIRCLE:                                                            
                      graphics.drawCircle(shapes[i].getP1(), shapes[i].getRadius());                
                      break;                                                                        
               }                                                                                    
           }                                                                                        
       }                                                                                            
    }
                                                                                              


    代码都是一直在改变的,而这也是上面的代码会碰到的一个问题.

    现在我们有一个问题: 如果我们需要支持更多的形状(比如三角形), 那么肯定要改动Shape这个类, CADApp里面的drawShapes这个方法也要改.
好,改为如下的样子:
    
class Shape {      
       final static int TYPELINE = 0;
       final static int TYPERECTANGLE = 1;
       final static int TYPECIRCLE = 2;
       final static int TYPETRIANGLE = 3;
       int shapeType;  
       Point p1;      
       Point p2;      
       //三角形的第三个点.
       Point p3;      
       int radius;    
    }                  
class CADApp {    
     void drawShapes(Graphics graphics, Shape shapes[]) {
         for (int i = 0; i < shapes.length; i++) {
             switch (shapes[i].getType()) {
                case Shape.TYPELINE:
                    graphics.drawLine(shapes[i].getP1(), shapes[i].getP2());
                    break;
                case Shape.TYPERECTANGLE:
                      //画四条边.
                    graphics.drawLine(...);
                    graphics.drawLine(...);
                    graphics.drawLine(...);
                    graphics.drawLine(...);
                    break;
                case Shape.TYPECIRCLE:
                    graphics.drawCircle(shapes[i].getP1(), shapes[i].getRadius());
                    break;
                case Shape.TYPETRIANGLE:
                    graphics.drawLine(shapes[i].getP1(), shapes[i].getP2());
                    graphics.drawLine(shapes[i].getP2(), shapes[i].getP3());
                    graphics.drawLine(shapes[i].getP3(), shapes[i].getP1());
                    break;
               }      
           }          
       }              
  }
                  

    如果以后要支持更多的形状,这些类又要改动……,这可不是什么好事情!
   理想情况下,我们希望当一个类,一个方法或其他的代码设计完以后,就不用再做修改了。它们应该稳定到不用修改就可以重用。
    现在的情况恰好相反!
    每当我们增加新的形状,都得修改Shape这个类,跟CADApp里面的drawShapes方法。

    怎么让代码稳定(也就是无需修改)?这个问题是个好问题!不过老规矩,先不说,我们以行动回答。
    我们先看看另外一个方法: 当给你一段代码,你怎么知道它是稳定的?


    怎么判断代码的稳定性?

    要判断代码的稳定性,我们可能会这样来判定:先假设一些具体的情况或者需求变动了,然后来看一看,要满足这些新的需求,代码是否需要被修改?
    可惜,这也是一件很麻烦的事,因为有那么多的可能性!我们怎么知道哪个可能性要考虑,哪些不用考虑?

    有个更简单的方法, 如果发现说,我们已经第三次修改这些代码了,那我们就认定这些代码是不稳定的。这个方法很“懒惰”,而且“被动”!我们被伤到了,才开始处理状况。不过至少这种方法还是一个很有效的方法。

    此外,还有一个简单,而且“主动”的方法:如果这段代码是不稳定或者有一些潜在问题的,那么代码往往会包含一些明显的痕迹。正如食物要腐坏之前,经常会发出一些异味一样(当然,食物如果有异味了,再怎么处理我们都不想吃了。但是代码可不行。)。我们管这些痕迹叫做“代码异味”。正如并不是所有的食物有异味都不能吃了,但大多数情况下,确实是不能吃了。并不是所有的代码异味都是坏事,但大多数情况下,它们确实是坏事情!因此,当我们感觉出有代码异味时,我们必须小心谨慎的检查了。

    现在,我们来看看上面例子中的代码异味吧。

    示例代码中的代码异味:

    第一种异味:代码用了类别代码(type code)。
  
class Shape {                                                                                  
     final int TYPELINE = 0;                                                                      
     final int TYPERECTANGLE = 1;                                                                
     final int TYPECIRCLE = 2;                                                                    
     int shapeType;                                                                              
     ...                                                                                          
  }    
                                                                                          

    这样的异味,是一种严肃的警告:我们的代码可能有许多问题。      

    第二种异味:Shape这个类有很多属性有时候是不用的。例如,radius这个属性只有在这个Shape是个圆的时候才用到:

    class Shape {                                                                                   
       ...                                                                                          
       Point p1;                                                                                    
       Point p2;                                                                                    
       int radius; //有时候不用                                                            
    }  
                                                                                            

    第三种异味:我们想给p1,p2取个好一点的变量名都做不到,因为不同的情况下,它们有不同的含义:

    class Shape {  
       ...        
       Point p1; //要取作“起始点”,“左下点”,还是“圆心”?
     Point p2;  
    }        
      

    第四种异味:drawShapes这个方法里面,有个switch表达式。当我们用到switch(或者一大串的if-then-else-if)时,小心了。switch表达式经常是跟类别代码(type code)同时出现的。
    现在,让我们将这个示例中的代码异味消除吧。

    消除代码异味:怎么去掉类别代码(type code)

    大多数情况下,要想去掉一个类别代码,我们会为每一种类别建立一个子类,比如:
    (当然,并不是每次要去掉一个类别代码都要增加一个新类,我们下面的另一个例子里面会讲另一种解决方法)

    class Shape {  
    }              
    class Line extends Shape {
       Point startPoint;
       Point endPoint;
    }              
    class Rectangle extends Shape {
       Point lowerLeftCorner;
       Point upperRightCorner;
    }              
    class Circle extends Shape {
       Point center;
       int radius;
    }              


    因为现在没有类别代码了,drawShapes这个方法里面,就要用instanceof来判断对象是哪一种形状了。因此,我们不能用switch了,而要改用if-then-else:
    class CADApp { 
       void drawShapes(Graphics graphics, Shape shapes[]) {
           for (int i = 0; i < shapes.length; i++) {
               if (shapes[i] instanceof Line) {
                  Line line = (Line)shapes[i];
                  graphics.drawLine(line.getStartPoint(),line.getEndPoint());
               } else if (shapes[i] instanceof Rectangle) {
                  Rectangle rect = (Rectangle)shapes[i];
                  graphics.drawLine(...);
                  graphics.drawLine(...);
                  graphics.drawLine(...);
                  graphics.drawLine(...);
               } else if (shapes[i] instanceof Circle) {
                  Circle circle = (Circle)shapes[i];
                  graphics.drawCircle(circle.getCenter(), circle.getRadius());                      
               }                                                                                    
           }                                                                                        
       }                                                                                            
    }    
                                                                                          

    因为没有类别代码了,现在每个类(Shape,Line,Rectangle,Circle)里面的所有属性就不会有时用得到有时用不到了。现在我们也可以给它们取一些好听点的名字了(比如在Line里面,p1这个属性可以改名为startPoint了)。现在四种异味只剩一种了,那就是,在drawShapes里面还是有一大串if-then-else-if。我们下一步,就是要去掉这长长的一串。
    

    消除代码异味:如何去掉一大串if-then-else-if(或者switch)       

    经常地,为了去掉if-then-else-if或者switch,我们需要先保证在每个条件分支下的要写的代码是一样的。在drawShapes这个方法里面,我们先以一个较抽象的方法(伪码)来写吧:
    
    class CADApp {                                                                                  
       void drawShapes(Graphics graphics, Shape shapes[]) {                                        
           for (int i = 0; i < shapes.length; i++) {                                                
               if (shapes[i] instanceof Line) {                                                    
                  画线条;                                                                    
               } else if (shapes[i] instanceof Rectangle) {                                        
                  画长方形;                                                              
               } else if (shapes[i] instanceof Circle) {                                            
                  画圆;                                                                  
               }                                                                                    
           }                                                                                        
       }                                                                                            
    }    
                                                                                          

    条件下的代码还是不怎么一样,不如再抽象一点:
    class CADApp {                                                                                  
       void drawShapes(Graphics graphics, Shape shapes[]) {                                        
           for (int i = 0; i < shapes.length; i++) {                                                
               if (shapes[i] instanceof Line) {                                                    
                  画出形状;                                                                  
               } else if (shapes[i] instanceof Rectangle) {                                        
                  画出形状;                                                                  
               } else if (shapes[i] instanceof Circle) {                                            
                  画出形状;                                                                  
               }                                                                                    
           }                                                                                        
       }                                                                                            
    }    
                                                                                          

    好,现在三个分支下的代码都一样了。我们也就不需要条件分支了:
    
    class CADApp {                                                                                  
       void drawShapes(Graphics graphics, Shape shapes[]) {
           for (int i = 0; i < shapes.length; i++) {
               画出形状;
           }    
       }        
    }      
    

    最后,将“画出形状”这个伪码写成代码吧:
    class CADApp {
       void drawShapes(Graphics graphics, Shape shapes[]) {
           for (int i = 0; i < shapes.length; i++) {
               shapes[i].draw(graphics);
           }    
       }        
    }        
  
    
    当然,我们需要在每种Shape的类里面提供draw这个方法:

    abstract class Shape {
       abstract void draw(Graphics graphics);
    }          
    class Line extends Shape {
       Point startPoint;
       Point endPoint;
       void draw(Graphics graphics) {
           graphics.drawLine(getStartPoint(), getEndPoint());
       }        
    }          
    class Rectangle extends Shape {
       Point lowerLeftCorner;
       Point upperRightCorner;
       void draw(Graphics graphics) {
           graphics.drawLine(...);
           graphics.drawLine(...);
           graphics.drawLine(...);
           graphics.drawLine(...);
       }        
    }          
    class Circle extends Shape {
       Point center;
       int radius;
       void draw(Graphics graphics) {
           graphics.drawCircle(getCenter(), getRadius());
       }        
    }
          

    将抽象类变成接口

    现在,看一下Shape这个类,它本身没有实际的方法。所以,它更应该是一个接口:


    interface Shape {                                                                               
       void draw(Graphics graphics);                                                                
    }                                                                                              
    class Line implements Shape {                                                                  
       ...                                                                                          
    }                                                                                              
    class Rectangle implements Shape {                                                              
       ...                                                                                          
    }                                                                                              
    class Circle implements Shape {                                                                
       ...                                                                                          
    }    
                                                                                          

    改进后的代码

    改进后的代码就像下面这样:
    
    interface Shape {                                                                               
       void draw(Graphics graphics);                                                                
    }                                                                                              
    class Line implements Shape {                                                                  
       Point startPoint;                                                                            
       Point endPoint;                                                                              
       void draw(Graphics graphics) {                                                              
           graphics.drawLine(getStartPoint(), getEndPoint());                                      
       }                                                                                            
    }                                                                                              
    class Rectangle implements Shape {                                                              
       Point lowerLeftCorner;                                                                      
       Point upperRightCorner;                                                                      
       void draw(Graphics graphics) {                                                              
           graphics.drawLine(...);                                                                  
           graphics.drawLine(...);                                                                  
           graphics.drawLine(...);                                                                  
           graphics.drawLine(...);                                                                  
       }                                                                                            
    }                                                                                              
    class Circle implements Shape {                                                                
       Point center;                                                                                
       int radius;                                                                                  
       void draw(Graphics graphics) {                                                              
           graphics.drawCircle(getCenter(), getRadius());                                          
       }                                                                                            
    }                                                                                              
    class CADApp {                                                                                  
       void drawShapes(Graphics graphics, Shape shapes[]) {                                        
           for (int i = 0; i < shapes.length; i++) {                                                
               shapes[i].draw(graphics);                                                            
           }                                                                                        
       }                                                                                            
    }
                                                                                              

    如果我们想要支持更多的图形(比如:三角形),上面没有一个类需要修改。我们只需要创建一个新的类Triangle就行了。



   另一个例子

    让我们来看一下另外一个例子。在当前的系统中,有三种用户:常规用户,管理员和游客。
    常规用户必须每隔90天修改一次密码(更频繁也行)。管理员必须每30天修改一次密码。游客就不需要修改了。
    常规用户跟管理员可以打印报表。
    先看一下当前的代码:

    class UserAccount {
       final static int USERTYPE_NORMAL = 0;
       final static int USERTYPE_ADMIN = 1;
       final static int USERTYPE_GUEST = 2;
       int userType;
       String id;  
       String name;
       String password;
       Date dateOfLastPasswdChange;
       public boolean checkPassword(String password) {
           ...    
       }          
    }              
    class InventoryApp {
       void login(UserAccount userLoggingIn, String password) {
           if (userLoggingIn.checkPassword(password)) {
               GregorianCalendar today = new GregorianCalendar();
               GregorianCalendar expiryDate = getAccountExpiryDate(userLoggingIn);
               if (today.after(expiryDate)) {
                  //提示用户修改密码
                  ...
               }  
           }      
       }          
       GregorianCalendar getAccountExpiryDate(UserAccount account) {
           int passwordMaxAgeInDays = getPasswordMaxAgeInDays(account);
           GregorianCalendar expiryDate = new GregorianCalendar();
           expiryDate.setTime(account.dateOfLastPasswdChange);
           expiryDate.add(Calendar.DAY_OF_MONTH, passwordMaxAgeInDays);
           return expiryDate;
       }          
       int getPasswordMaxAgeInDays(UserAccount account) {
           switch (account.getType()) {
               case UserAccount.USERTYPE_NORMAL:
                  return 90;
               case UserAccount.USERTYPE_ADMIN:
                  return 30;
               case UserAccount.USERTYPE_GUEST:
                  return Integer.MAX_VALUE;
           }      
       }          
       void printReport(UserAccount currentUser) {
           boolean canPrint;
           switch (currentUser.getType()) {
               case UserAccount.USERTYPE_NORMAL:
                  canPrint = true;
                  break;
               case UserAccount.USERTYPE_ADMIN:
                  canPrint = true;
                  break;                                                                            
               case UserAccount.USERTYPE_GUEST:                                                    
                  canPrint = false;                                                                
           }                                                                                        
           if (!canPrint) {                                                                        
               throw new SecurityException("You have no right");                                    
           }                                                                                        
           //打印报表
       }                                                                                            
    }
                                                                                              

     用一个对象代替一种类别(注意,之前是一个类代替一种类别)

    根据之前讲的解决方法,要去掉类别代码,我们只需要为每种类别创建一个子类,比如:
    
    abstract class UserAccount {                                                                    
       String id;                                                                                  
       String name;                                                                                
       String password;                                                                            
       Date dateOfLastPasswdChange;                                                                
       abstract int getPasswordMaxAgeInDays();                                                      
       abstract boolean canPrintReport();                                                          
    }                                                                                              
    class NormalUserAccount extends UserAccount {                                                  
       int getPasswordMaxAgeInDays() {                                                              
           return 90;                                                                              
       }                                                                                            
       boolean canPrintReport() {                                                                  
           return true;                                                                            
       }                                                                                            
    }                                                                                              
    class AdminUserAccount extends UserAccount {                                                    
       int getPasswordMaxAgeInDays() {                                                              
           return 30;                                                                              
       }                                                                                            
       boolean canPrintReport() {                                                                  
           return true;                                                                            
       }                                                                                            
    }                                                                                              
    class GuestUserAccount extends UserAccount {                                                    
       int getPasswordMaxAgeInDays() {                                                              
           return Integer.MAX_VALUE;                                                                
       }                                                                                            
       boolean canPrintReport() {                                                                  
           return false;                                                                            
       }                                                                                            
    }
                                                                                              

    但问题是,三种子类的行为(里面的代码)都差不多一样,getPasswordMaxAgeInDays这个方法就一个数值不同(30,90或者Integer.MAX_VALUE)。canPrintReport这个方法也不同在一个数值(true或false)。这三种用户类型只需要用三个对象代替就行了,无须特地新建三个子类了:

    class UserAccount {
       UserType userType;
       String id;  
       String name;
       String password;
       Date dateOfLastPasswdChange;
       UserType getType() {
           return userType;
       }          
    }              
    class UserType {
       int passwordMaxAgeInDays;
       boolean allowedToPrintReport;
       UserType(int passwordMaxAgeInDays, boolean allowedToPrintReport) {
           this.passwordMaxAgeInDays = passwordMaxAgeInDays;
           this.allowedToPrintReport = allowedToPrintReport;
       }          
       int getPasswordMaxAgeInDays() {
           return passwordMaxAgeInDays;
       }          
       boolean canPrintReport() {
           return allowedToPrintReport;
       }          
       static UserType normalUserType = new UserType(90, true);
       static UserType adminUserType = new UserType(30, true);
       static UserType guestUserType = new UserType(Integer.MAX_VALUE, false);
    }              
    class InventoryApp {
       void login(UserAccount userLoggingIn, String password) {
           if (userLoggingIn.checkPassword(password)) {
               GregorianCalendar today = new GregorianCalendar();
               GregorianCalendar expiryDate = getAccountExpiryDate(userLoggingIn);
               if (today.after(expiryDate)) {
                  //提示用户修改密码
                  ...
               }  
           }      
       }          
       GregorianCalendar getAccountExpiryDate(UserAccount account) {
           int passwordMaxAgeInDays = getPasswordMaxAgeInDays(account);
           GregorianCalendar expiryDate = new GregorianCalendar();
           expiryDate.setTime(account.dateOfLastPasswdChange);
           expiryDate.add(Calendar.DAY_OF_MONTH, passwordMaxAgeInDays);
           return expiryDate;
       }          
       int getPasswordMaxAgeInDays(UserAccount account) {
           return account.getType().getPasswordMaxAgeInDays();
       }          
       void printReport(UserAccount currentUser) {
           boolean canPrint;
           canPrint = currentUser.getType().canPrintReport();
           if (!canPrint) {
               throw new SecurityException("You have no right");
           }      
           //打印报表.
       }          
    }
                                                                                              

    注意到了吧,用一个对象代替类别,同样可以移除switch或者if-then-else-if。      

总结一下类别代码的移除

    要移动一些类别代码和switch表达式,有两种方法:                            
    1.用基于同一父类的不同子类来代替不同的类别。                                        
    2.用一个类的不同对象来代替不同的类别。
    当不同的类别具有比较多不同的行为时,用第一种方法。当这些类别的行为非常相似,或者只是差别在一些值上面的时候,用第二个方法。

     普遍的代码异味
    类别代码和switch表达式是比较普遍的代码异味。此外,还有其他的代码异味也很普遍。
    下面是大概的异味列表:
    代码重复
    太多的注释
    类别代码(type code)

   switch或者一大串if-then-else-if
    想给一个变量,方法或者类名取个好名字时,也怎么也取不好    
    用类似XXXUtil, XXXManager, XXXController 和其他的一些命名
    在变量,方法或类名中使用这些单词“And”,“Or”等等  
    一些实例中的变量有时有用,有时没用
    一个方法的代码太多,或者说方法太长
    一个类的代码太多,或者说类太长
    一个方法有太多参数
    两个类都引用了彼此(依赖于彼此)

资源
下载PDF文件
2007-09-21 14:36:11 pengchua 阅读数 71
  • SCRUM敏捷开发视频教程

    SCRUM敏捷开发视频教程,该课程为你分享SCRUM敏捷开发,理解敏捷的本质,认识中国IT行业对敏捷的挑战,学会让敏捷落地的实用招数。 嘉宾介绍:张传波 1. 创新工场创业课程(敏捷课程)讲师 2.软件研发管理佳实践顾问(曾任华为某团队研发顾问) 3. 中国敏捷联盟《ADBOK》(敏捷开发知识体系)项目组成员 二十年软件开发、软件设计、需求分析、项目管理、部门管理、公司管理及过程改进等经验,亲历“无数”项目,涉猎建筑、通讯、互联网、电力、金融、制造业、政府等领域,熟悉软件生命周期的全部过程

    10408 人正在学习 去看看 CSDN讲师
敏捷开发的几大技巧:
1、移动重复代码;
2、将注解转换为代码:将注释转换为代码,让代码足够清楚到可以表示注释,如将一部分代码重构成方法,用方法名来

表达注释的意思。
3:除去代码异味:
   第一种异味:代码用了类别代码(type code)如final int TYPERECTANGLE = 1;
   第二种异味:Shape 这个类有很多属性有时候是不用的;
   第三种异味:我们想给p1,p2 取个好一点的变量名都做不到,因为不同的情况下,它们有不同的含义:
   第四种异味:drawShapes这个方法里面,有个switch表达式。当我们用到switch(或者一大串的if-then-else-

if)时,小心了。switch表达式经常是跟类别代码(type code)同时出现的。
    消除代码异味方法:
    针对第一种,大多数情况下,要想去掉一个类别代码,我们会为每一种类别建立一个子类.再使用就要用

instanceof来判断对象。
    要移动一些类别代码和switch表达式,有两种方法:
    a.用基于同一父类的不同子类来代替不同的类别。
    b.用一个类的不同对象来代替不同的类别。
    
4.保持代码简洁
要判断一个类是否需要修整,一个比较主观的方法是:当在读一个类的代码时,看看我们会不会觉得这个类
“太长了”,“太复杂了”,或者讲的概念“太多了”?如果会这样觉得的话,我们就认定,这个类需要修整.
单一职责原则(The Single Responsibility Principle)认为:每个类都应该只为一个理由而修改.
5.慎用继承:
当我们想要让一个类继承自另一个类时,我们一定要再三的检查:子类会不会继承了一些它不需要的功能(属性或

者方法)?如果是的话,我们就得认真再想想:它们之间有没有真正的继承关系?如果没有的话,就用代理。如果

有的话,将这些不用的功能从基类转移到另外一个合适的地方去。
如果一个父类描述的东西不是所有的子类共有的,那这个父类的设计肯定不是一个好的设计。

里斯科夫替换原则(LSP)表述:Subtype must be substitutable for their base types. 子类应该能够代替父类的
功能。或者直接点说,我们应该做到,将所有使用父类的地方改成使用子类后,对结果一点影响都没有。或者更
直白一点吧,请尽量不要用重载,重载(类中可以相同的方法名不同的参数)是个很坏很坏的主意。

6处理不合适的依赖
  怎么判断是“不合适的依赖”
  方法1:
一个简单的方法就是:我们先看一下这段代码里面有没有一些互相循环的引用。比如,ZipMainFrame引用了
ZipEngine这个类,而ZipEngine又引用了ZipMainFrame。我们管这样的类叫“互相依赖”。互相依赖也是一种代
码异味,我们就认定这样的代码,是“不合适的依赖”。
  方法2:
另一个方法比较主观:在检查代码的时候,我们问自己:对于它已经引用的这些类,是它真正需要引用的吗?
  方法3:
第3 种方法也很主观:在设计类的时候,我们先预测一个以后可能会重用这个类的系统。然后再判断,在那
样的系统中,这个类能不能被重用?如果你自己都觉得以后的系统不能重用这个类的话,你就断定,这个类包含
“不合适的依赖”了。
   方法4
第4 个方法比较简单而且客观了。当我们想在一个新系统中重用这个类,却发现重用不了时,我们就判断,
这个类包含了“不合适的依赖”。
解决方法:除掉其中的依赖类,利用接口来处理。
依赖反转原则(Dependency Inversion Principle )表述:抽象不应该依赖于具体,高层的比较抽象的类不应该
依赖于低层的比较具体的类。当这种问题出现的时候,我们应该抽取出更抽象的一个概念,然后让这两个类依赖
于这个抽取出来的概念
7、 将数据库访问,UI和域逻辑分离
  先抽取出数据库访问层,然后将领域逻辑将表示层也分离。
8、以用户例事管理项目  (也就通常我们所说的用例)
   一件用户通过系统完成他一个有价值的目标(买一罐饮料)的事。这样的过程就叫“用户案例(user case)”或

者“用户例事(user story)”。也就是说,上面我们的客户所说的话,就是在描述一个用户例事(user story)。
9、 用CRC卡协助设计
    写上了类名,它的职责,以及它的协作关系,我们管这样的卡片叫“CRC卡”。CRC就是Class,Responsibility

和Collaboration的简称
10、 验收测试
11、 对UI进行验收测试
12、 单元测试
13、 测试驱动编程
14、 结对编程
      结对编程的好处:
    联合两人的知识去对付一个难题。
    知识互相传递。
    更有效的查错跟纠错。
    程序员都很开心。
    减少员工离职的损失。
     结对编程需要的一些技能:
    用代码解释已有的设计结构。
    用例子来解释。
    用图表来解释设计思路。
    如果你无法把你的设计思路表达清楚,把代码写出来。
    让比较迷惑的搭档来写代码,这样他就可以较好的融入你的概念。
    经常的休息。
    经常的更换搭档

2008-03-05 10:32:00 Ronbi 阅读数 496
  • SCRUM敏捷开发视频教程

    SCRUM敏捷开发视频教程,该课程为你分享SCRUM敏捷开发,理解敏捷的本质,认识中国IT行业对敏捷的挑战,学会让敏捷落地的实用招数。 嘉宾介绍:张传波 1. 创新工场创业课程(敏捷课程)讲师 2.软件研发管理佳实践顾问(曾任华为某团队研发顾问) 3. 中国敏捷联盟《ADBOK》(敏捷开发知识体系)项目组成员 二十年软件开发、软件设计、需求分析、项目管理、部门管理、公司管理及过程改进等经验,亲历“无数”项目,涉猎建筑、通讯、互联网、电力、金融、制造业、政府等领域,熟悉软件生命周期的全部过程

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

本文转自
http://www.bitscn.com/java/base/200709/110072.html
2008-05-13 18:27:00 bluedusk 阅读数 44
  • SCRUM敏捷开发视频教程

    SCRUM敏捷开发视频教程,该课程为你分享SCRUM敏捷开发,理解敏捷的本质,认识中国IT行业对敏捷的挑战,学会让敏捷落地的实用招数。 嘉宾介绍:张传波 1. 创新工场创业课程(敏捷课程)讲师 2.软件研发管理佳实践顾问(曾任华为某团队研发顾问) 3. 中国敏捷联盟《ADBOK》(敏捷开发知识体系)项目组成员 二十年软件开发、软件设计、需求分析、项目管理、部门管理、公司管理及过程改进等经验,亲历“无数”项目,涉猎建筑、通讯、互联网、电力、金融、制造业、政府等领域,熟悉软件生命周期的全部过程

    10408 人正在学习 去看看 CSDN讲师

1、移除重复代码

     其根本方法是把大于1次使用的代码分离成共有方法。
     重构的目标应该是在不降低代码效率的基础上提高代码的复用性和质量。重构应该考虑效率、结构、代价之间的平衡。

2、把注释化为代码

     看到代码就能见文知义,消除无谓注释。一般注释用于说明该处的想法、算法、优劣、记录等,能用代码说明的尽量不要用注释。
     当我们要加注释的时候,我们应该再三的想想:
     注释能不能转化在代码里面,让代码跟注释一样的清晰?
     大多数情况下是能!
     每一个注释都是一个改进代码的好机会!
     包含太多注释的代码,绝对不是高质量的代码!

3、除去代码异味

     理想情况下,我们希望一个类、方法或其他代码设计完成以后,就不用再做修改了。它们应该稳定到不用修改就能重用。

     判断代码的稳定性,我们可能会这样:先假设一些具体的情况或者需求改变了,然后来看一看,要满足这些新的需求,代码是否需要被修改?

     有个简单的方法,如果我们发现已经第三次修改这些代码了,那我们就认定这些代码是不稳定的。这方法比较“懒惰”,而且“被动”,虽然这种方法还算有效。

     还有一个简单的方法,而且“主动”。如果这段代码是不稳定的或者有一些潜在问题,那么代码往往会包含一些明显的痕迹。这些痕迹就是“代码异味”。

     普遍的代码异味:

    类别代码和switch表达式是比较普遍的代码异味。此外,还有其他的代码异味也很普遍。
    下面是大概的异味列表:
    代码重复
    太多的注释
    类别代码(type code)
    switch或者一大串if-then-else-if
    想给一个变量,方法或者类名取个好名字时,也怎么也取不好    
    用类似XXXUtil, XXXManager, XXXController 和其他的一些命名 
    在变量,方法或类名中使用这些单词“And”,“Or”等等   
    一些实例中的变量有时有用,有时没用
    一个方法的代码太多,或者说方法太长
    一个类的代码太多,或者说类太长
    一个方法有太多参数
    两个类都引用了彼此(依赖于彼此)

要移动一些类别代码和switch表达式,有两种方法:                            
    1.用基于同一父类的不同子类来代替不同的类别。                                        
    2.用一个类的不同对象来代替不同的类别。
    当不同的类别具有比较多不同的行为时,用第一种方法。当这些类别的行为非常相似,或者只是差别在一些值上面的时候,用第二个方法。

4、保持代码简洁

    判断一个类是否需要修整,一个比较主观的方法是:看看我们会不会觉得这个类“太长了”,“太复杂了”,或者包含的概念“太多了”?如果会这样觉得的话,我们就认定这个类需要修整。

    另外一个比较简单(虽然懒惰、被动)而且客观的方法是:当发现我们已经在第二次或者第三次扩充这个类的时候,我们认定这个类药修整了。

5、慎用继承

    Java中传递调用的中间介叫“代理(delegation)”。其实就是和继承并列的组合。

    如果一个父类描述的东西不是所有的子类共有的,那这个父类的设计肯定不是一个好的设计。

    当我们想要让一个类继承自另一个类时,我们一定要再三的检查:子类会不会继承了一些它不需要的功能(属性或者方法)?如果是的话,我们就得认真再想想:它们之间有没有真正的继承关系?如果没有的话,就用代理。如果有的话,将这些不用的功能从基类转移到另外一个合适的地方去。

6、处理不合适的引用(依赖)

    互相依赖也是一种代码异味,我们就认定这样的代码,是“不合适的依赖”。

要判断一个代码是不是包含了“不合适的依赖”,共有四个方法:

   1.看代码:有没有互相依赖?

   2.认真想想:它真正需要的是什么?

   3.推测一下:它在以后的系统中可以重用吗?

   4.到要重用的时候就知道了:现在我要重用这个类,能不能重用?

方法1跟4是最简单的方法,推荐初学者可以这样来判断。有更多的设计经验了,再用方法2跟3会好一些。

7、将数据库访问、UI和域逻辑分离

     域逻辑,原文是叫Domain logic,“Domain logic is also called "domain model" or
       "business logic".”,即“域逻辑又称为域模型或者业务逻辑”。

     这三个不同类别的代码混在一起,会造成下面的问题:
     1.代码很复杂。
     2.代码很难重用。如果是在一个web系统中,就更难重用了。
     3.代码很难测试。每次要测这样的一段代码,我们都要建一个数据库,还要通过一个用户操作界面来测试。
     4.如果数据库表结构更改了,很多地方都要跟着更改。
     5.它导致我们一直在考虑一些低层的太细节的概念,比如数据库字段,表的记录之类的,而不是类,对象,方法和属性这一类的概念。或者说白了一点,一直在考虑怎么往数据库里面装数据,而没有了面向对象的概念,没有了建立业务模型的思维。

    因此,我们应该将这三种类别的代码分离开(UI,数据库访问,域逻辑)。 

    一般而言,比较完美的方案是UI层和数据库访问层依赖于域逻辑层,那么域逻辑就能得到最大程度的复用,而这就是我们设计结构的目标(特别是大项目)。

    很多东西都属于UI层,不仅仅窗口、按钮这些,报表,Servlet,Jsp,文本控制台等等也算。搞J2EE开发的要注意,请不要在Servlet,Jsp里面放置任何的域逻辑或者数据库访问的代码;也不要在域逻辑中调用UI的代码,如System.out.print.    

8、以用户例事管理项目

    用户例事 User Case or User Story

    在一个系统面前,每个用户要完成同样的目标,都要做这个系统设定的例行的事,这件事情不是一个例子,所以不叫事例,这也不是故事,也不能算一段历程,而是一个例行的事。

    一个用户例事只是以客户能明白的方式,描述了一个系统的外在行为,它完全忽略了系统的内部动作。千万不要提及任何有关数据库、记录、字段之类的对客户一点意义都没有的东西。

    一开始我们可以用例事点来评估发布时间。

    为了做到更准的估计,我们希望客户给我们一段时间做些实际的开发,来测量一下我们在这段时间里面可以做多少用户例事,这段时间就叫“迭代周期”。通过迭代周期,我们可以进一步评估发布时间。

    如果不能如期完成,一种方法是我们和客户达成协议推迟发布周期,我们先选择客户认为比较重要的例事点来实现,而把相对次要的放在下一个发布周期去;另一种方法,可以增加开发人员来满足发布期限,但是这里要注意,团队人数加倍不等于开发周期的减半,如果团队超过10个人,增加更多的人可能反而会延缓项目的进度,项目开发周期越长,团队内的成员对整个项目的代码熟悉度越少,加上不确定的人员流动,这个项目会越来越复杂。

9、用CRC卡协助设计

    CRC(Class,Responsibility,Collaboration),在卡里写上类名,它的职责,以及它的协作关系。

    之所以用CRC卡,因为

    1)卡片上的空间小,可以防止我们给这个类太多的职责。如果职责过多的话(比如大于4个),尝试以更抽象的方式去考虑一下,将职责划分。

    2)CRC卡主要是用在探索或者讨论类的设计阶段。如果我们觉得这个设计不好,既不用修改文档,也不用修改类图,只要把卡片丢了就行。此外,一旦设计完成,我们就可以把所有的卡丢了,它们不是用来做文档的。

    3)如果我们觉得现在的卡片不合适,之前设计的比较好,我们只要简单的把之前的卡片拿出来组合就行了。

    CRC卡主要是用来快速的组织设计。不应该花很长时间做CRC卡,也不要指望按照CRC卡的设计就一切ok。在编码的时候,肯定还会不断地调整设计。将设计和编码结合起来,我们才能做出好而有效的设计。

    一句话,CRC卡是用来帮忙清理设计的思路,它不是UML图,也不是精确的类结构图。只要我们在处理这些卡的时候不断讨论,我们设计的思路将会变得非常清楚。

10、验收测试

     验收测试,也叫功能测试,只是测试系统的外部行为,忽略系统里面有哪些类,哪些模块。

     由于手动测试用例要花很多的时间和精力,我们希望这些测试用例可以在不需要人为干预的情况下自动运行。这样的测试,叫“自动验收测试”。

     测试代码是需要调用“正式”代码的,而“正式”代码绝对不能调用测试代码。

     测试代码应该在实现用户例事之前写。测试用力使我们软件需求的一个重要组成部分(现场客户是另一个重要部分)。作为需求的一部分,测试用例可以知道我们实现用户例事。而现场客户的优点是,最新的信息,更有效的沟通;而测试用例的优点则是更准确,自动化的测试则可以很频繁地运行。

    测试文件不一定是个文本文件,可以是excel,也可以是html或者其他各式的文件,只要我们可以解析出来就可以了。最关键的一点是客户能够明白和操作这些测试文件。

    用测试用例防止系统走下坡路。每次我们跟客户一起建立了一个测试文件,我们都把它放在一个名为“测试队列”的文件夹里。开始的时候所有的测试文件都放在这边。每运行通过一个测试文件,我们就将这个测试放到另外一个叫“通过”的文件夹里。之后不管是重构,还是增加新的功能,或者修改代码以后,都要将“通过”这个文件夹里的所有测试文件跑一边。如果有测试文件通不过,证明代码有错,我们马上修改代码,让它通过。而不是将通不过的测试文件放回“测试队列”的文件夹里。也就是说,除非是测试文件本身出了问题,否则,绝对不能把测试文件从“通过”放回到“测试队列”中去。当最终“测试队列”里面的文件都移到“通过”队列后,系统完成了。

11、对UI进行验收测试

     UI测试的原则是:分开测试每个UI组件。

12、单元测试

     验收测试测试的是系统的外部行为,单元测试是测试系统的内部结构,它只测一个单元(类、甚至一个方法)。验收测试属于客户的,我们没有权利决定验收测试的内容,我们顶多只是帮忙客户根据用户例事写出验收测试。单元测试属于我们,我们只是根据我们对这个单元的期望写出单元测试。

13、测试驱动编程

    代码写得越复杂,我们就越担心;写得越多,我们也越担心。这时候,测试先行的方法就值得考虑了。

    测一点,写一点的好处:如果我们弄出一个bug,它会马上被发现,然后很容易定位到出错源。

    我们要在实现代码之前,保证我们的测试通不过。如果测试通过了,没有像预期中的失败了,说明我们的测试有问题。 

    我们要测试代码是不是如我们预期的调用了某一个方法,我们要判断2点:1。它确实调用了我们希望它调用的方法。2。它调用时传递过去当参数的对象,应该是我们预期的对象。

    TDD(Test Driven Development)的优点:

    1,为了更容易写单元测试,我们会广泛地使用接口。这个会让单元测试代码很容易读写,因为测试代码里面没有多余的数据。

    2,因为广泛的使用接口,我们的类之间就不会耦合,因此重用性更好。

    3,写单元测试的时候,很容易就可以为一个行为写一个测试用例,让它通过,然后为另一种行为写另一个测试用例。也就是说,整个任务会被划分成很多小的任务,独立完成。

    要做什么,不要做什么:

    1,不要再测试里包含多余的数据,为此,你会用更多地使用接口和匿名类。

    2,如果你需要写一堆代码来建立测试上下文(比如数据库连接),那你绝对不要忍受这种痛苦。如果它确实发生了,那就用接口吧。

    3, 不要在两个测试里面测试了同样的东西。

    4,在写出通不过的测试之前,绝对不要写代码,除非代码非常简单。

    5,不要(至少尽量避免)一次性将所有的测试弄坏。

    6,不要一次性写太多东西,或者做一些要花费几个小时的事情。尽量将这些东西划分为更小的行为,使用TODD列表。

    7,每写完一个测试用例,在几分钟内通过它,并不是放在一边。

    6,每做一个或者一些小改动后,就要运行所有测试用例。

    9,先挑出你比较有兴趣的,或者可以从中尝到东西的任务,而不是先挑那些烦人的任务。

    10,测试调用顺序的时候,一定要使用调用日记。

    11,任何时候,脑中都要有个完整的概念。

14、结对编程

    如果我们不懂对方在说什么,最好的办法就是让他举个例子。这是沟通(也是结对编程中)最重要的方法。

    有经验的开发人员,有时候还是可以从年青的开发人员上学到一些东西。

    交流设计的最好方法是边看代码边解释。如果还没有代码,就用图表。

    在结对编程里,一起设计是主要活动之一。除此,一起测试、一起查错也是另两个主要活动。

    在结对编程里,有着不同的知识/技能的人可以把他们的技能共享来解决一个困难的问题。经常可以交流知识和好的实践经验。开发人员会比较开心,做事也比较有信心。

    如果我们不能明白另一个人的设计意图,那最好的方法就是,写代码。让比较不明白的那个人来写。

    即使一个人在某一方面很擅长,另一个人还是有空间做一些有意义的贡献的。有经验不代表每次都能更快找到错误。每个人都可能擅长找某一方面的问题,而不擅长于其他方面的。幸运的是,两个人的“盲区”通常不会重叠。

    一起写代码也是结对编程的主要活动。

    因为结对编程两人的精力都会很集中,精神容易紧张,所以经常的休息开发效率才会更高。

    在这次结对编程以后,两个人对代码和业务都更熟悉了。如果他们中有人要去休假或者离开公司的话,另一个也可以维护。也就是说,结对编程可以减少职工离职对公司的损失。

    经常地换搭档是好事(特别是在一个新任务的开始阶段)。现在其中一个可以继续把他懂的东西教给第三个人,同时也能从第三个人身上学到另外一些。

    结对编程的好处:

    1,联合两人的知识去对付一个难题。

    2,知识互相传递。

    3,更有效的差错跟纠错。

    4,程序员都很开心。

    5,减少员工离职的损失。

    结对编程需要的一些技能:

    1,用代码解释已有的设计结构。

    2,用例子来解释。

    3,用图表来解释设计思路。

    4,如果你无法把你的设计思路表达清楚,把代码写出来。

    5,让比较迷惑的搭档来写代码,这样他就可以较好地融入你的概念。

    6,经常地休息。

    7,经常地更换搭档。

    研究表明,在一个大学的环境里,让两个人做一件事情,花费的时间比两个人分工所需的时间多15%。也就是说,我们没必要加倍。所以一方面来讲,结对编程有很大的好处,但同时也要多花费15%的开发时间。但研究还表明,结对编程开发出来的软件,bug的数量比分工开发出来的少不止15%。单人修复bug所花的时间,是结对的人所花时间的15-60倍。很明显,结对编程远远抵消了那多费的15%的开发时间。

    结对编程不是万灵的。因为它需要两个人不断地沟通,一起做决定,如果不能沟通或者做不了决定的话,结对编程就行不通了。下面是常见的问题,会造成结对编程无法正常工作:

    1,不情愿的配合。

    2,拒绝别人的意见,甚至攻击对方。

    3,小心翼翼有意见不敢提。

    4,怕别人觉得自己笨不敢问问题。

   注:

开闭原则(Open Closed Principle):如果我们需要增加新的功能,我们只需要增加新的代码,而不是改变原有的。移除switch和类别代码是达到开闭原则的普遍方法.

单一职责原则(The Single Responsibility Principle):每个类都应该只为一个理由而修改。当一个类包含许多其他功能时,很明显违反了单一职责原则。

里斯科夫替换原则(LSP)(Subtype must be substitutable for their base types): 子类应该能够代替父类的功能。或者直接点说,我们应该做到,将所有使用父类的地方改成使用子类后,对结果一点影响都没有。或者更直白一点吧,请尽量不要用重载,重载是个很坏很坏的主意!

依赖反转原则(Dependency Inversion Principle ):抽象不应该依赖于具体,高层的比较抽象的类不应该依赖于低层的比较具体的类。当这种问题出现的时候,我们应该抽取出更抽象的一个概念,然后让这两个类依赖于这个抽取出来的概念。

《重构——改善既有代码的设计》
下面粗略地概括一下对重构的理解,也整理一下之前不是很清楚的概念。

1、《重构》有一个很好的动机,也可以说是价值观,就是程序第一是写给人看的,而不是写给机器看的。
根据这一价值观,其他多种利益纷至沓来,比如当程序有了良好的可读性和可理解性,程序中隐藏的Bug便很容易浮出水面,开发进度也更加顺畅,并且对于系统将来的结构变更和扩展,程序也更加具有灵活性。

2、《重构》与《设计模式》的关系,在《设计模式》和《重构》中都有提出“设计模式为重构提供了目标”,在之前对这句话的理解总是朦朦胧胧,觉得有道理但又不是很深刻,现在觉得有两个词非常的关键:目标和目的。

设计模式为重构提供了目标,但不是目的。

设计模式是经过证实的在一定场景下解决一般设计问题的解决方案的核心,通过设计模式我们很好得解决了某种问题,并且便于我们思考和交流,降低沟通之间的理解误差,此外同样重要的,设计模式增强了可复用性,便于将来扩展和维护。

而重构是对程序内部结构的一种调整,其目的是在不改变“软件之可察行为”的前提下,提高其可理解性,降低其修改成本(《重构》的名词性定义)。

所以如果我们把软件开发比作在大海中航行,设计模式就是遍布在大海中的航标,他可以引导我们驶向目的地——高可读性、可理解性、可扩展性、可维护性。所以设计模式是重构的目标(航标)而不是目的,设计模式可以帮助我们更好更快的抵达目的地(准确地说是无止境的),而避免触礁或偏离航向

3、重构和优化,在之前的开发中,优化的意识要比现在(看完《重构》之后)强的多,如果遇到在一个循环中可以做多个事情的时候,决定把每件事情分开放到单独的循环中是要鼓起很大的勇气的,而现在便可以轻松的决定,因为清晰的代码在需要性能优化时有更宽的道路可以选择,并且往往这种决定不会造成真正的性能影响。

《实时UML与Rational Rose RealTime建模案例剖析 》

将实时系统、实时统一建模语言、实时系统的统一开发过程和Rational Rose RealTime建模环境有机地结合起来,以案例为基础,系统地介绍了实时系统的设计与实现。全书分为3部分,第1部分为基础篇,主要介绍实时系统的基本概念、实时统一建模语言、实时对象约束语言和Rational Rose RealTime建模环境。第2部分为建模篇,结合实时统一建模语言和Rational Rose RealTime建模工具,介绍了实时系统的需求分析、系统设计和实现与部署。第3部分为案例篇,分析了4个典型的实时系统案例:纸牌游戏、咖啡机控制系统、ATM自动取款机控制系统和电梯控制系统的设计与实现。案例是针对不同层次的实时系统开发人员进行设计的,同时也涵盖了实时系统设计的主要特性。

没有更多推荐了,返回首页