文/胡振波

BOSCO系统是一个在线品牌管理系统,项目客户是一家跨国酒店集团,旗下拥有多个世界著名的酒店品牌。BOSCO系统将服务标准化、标准符合度审查、改进流程管理等酒店品牌管理的工作内容整合到一个信息系统中,来提高相关人员的工作效率。目前BOSCO系统已经被全球十个酒店品牌、超过1000家酒店使用,用户超过8000人。

BOSCO系统的开发基于Ruby On Rails,在项目的开发过程中应用了敏捷开发方法。在开发此系统的8个月中,经历了15个迭代和3次发布。通过这种与客户紧密合作的工作方式我们按时交付了系统,并得到了客户的高度认可。笔者作为开发团队的一员,从开发者的角度对基于Ruby On Rails的敏捷开发实践总结出一些心得体会,其中有成功的经验,也有失败的教训,愿与敏捷开发爱好者分享。

在我们的系统中,有一个有趣的类,叫做TimeMachine,用于修改UAT服务器的系统时间。但现在,让它带我们穿梭时空吧。

TimeMachine.go_to("2008-04-15")

半路接手——关键字:Knowledge Transfer,结对编程

2008年4月,BOSCO系统漂洋过海来到中国。此时,它已经在美国经过了3个月的开发,且完成了第一次发布。现在要开始它的移植过程(从美国团队的大脑移植到中国团队的大脑)。给我们的资源并不多,只有3个项目原有工作人员:一个BA(业务分析师),两个Dev(开发人员)。但时间却相当有限:一个月。

虽然BOSCO系统的开发时间并不长,但Rails强大的表现力使系统已经足够庞大,我们也担忧Knowledge Transfer能否在约定的时间内完成。而且中国的开发团队中,真正有Rails开发经验的只有一人。如何在这么短的时间内,掌握这么庞大的系统,成了摆在眼前的挑战。

很多遇到过此种情况的开发人员,都不会对以下几种经历感到陌生:

●  阅读大量关于系统原理和项目架构的文档,但它们并不能直观、准确地反应项目情况。

●  参加各种标榜着“Knowledge Transfer”且耗时长久的会议,但收益甚少。

●  独自面对系统时不知从何下手,不断地寻求原有开发人员的帮助。

●  由于不熟悉系统造成在开发过程中不断犯错,贡献了很多“垃圾”代码,并影响了系统开发进度。

其实对于开发人员而言,掌握一个项目最重要的是提升对项目本身的“熟悉度”。此熟悉度代表对陌生的技术知识、代码的编写风格、开发的习惯、程序的架构、环境的搭建等等的掌握和了解。而结对编程是快速掌握这些知识技能的秘诀,还是拿事实来说话吧。

两个星期之后,先行和两个美国Dev结对的同事已经能够带新人了,而且开发速度不亚于两个美国 Dev。短短两个礼拜,中国团队对项目的整体架构,不说了然于心,也是熟门熟路;对于一些story,也能得心应手开始实现。这是我们自己也预想不到的速度。

之所以能在如此短的时间内掌握项目,就是拜“结对编程”所带来的好处所赐。有人会说,结对编程到底有什么魔法?其实没有魔法,它只是简单实践了很多人都懂得的道理:学习一件东西最快速的手段就是动手去做。而结对编程,不仅能让你有立即动手去做的机会,而且边上还有个让你观察学习、给你指导的老师。这是我对结对编程的初次体验,让我非常兴奋。还记得一个朋友给我电话告诉我“项目中的新人不能快速掌握知识”时,我告诉他:不要犹豫了,结对编程吧。我相信,一旦你开始尝试,你就不愿意停止。

TimeMachine.go_to("2008-05-15")

加速前进——关键字:最佳实践

随着Knowledge Transfer期满、美国同事离去,客户一度很担心中国团队能否掌控项目,能否保持开发速度。但在快速接手项目之后,项目开发随即进入了加速通道。在完全接手项目的第一个迭代之后,中国团队已经赶上了美国同事的开发速度。在这个过程中,除了因为团队的快速学习能力之外,还有我们保持的一些最佳实践让我们赢得了胜利。下面让我简单介绍其中一些。

一致的开发机器。我们有6个开发人员,也就是3对pair。Pair的3台机器都是漂亮精致的Mac Mini。当然这不是重点,重点在于,3台机器都有一致的开发环境:一致的程序目录,一致的安装软件,一致的快捷命令等等。完全一模一样的配置使pair在进行切换时,不会对另外一台机器感到丝毫的陌生,这有助于开发人员迅速进入开发状态。

快捷化常用命令。在开发时,我们每天都要进行无数次相同的一些操作:进入Rails开发根目录,启动Web server,签入代码等等。比如当你要进入Rails开发目录,你就得打入命令:cd /Users/rails/workspace/bosco/rails_root。这是一个简单的命令,但同时也是一个繁琐的命令,特别是当我们每天要进行多次这样的操作时,就会浪费大量的时间。这时,如果你为它加一个alias,把这个枯燥的操作用一个简单的命令“rr”代替,它会帮你节省多少时间呢?效率的提升有时就源于一些简单的改善。

不做简单重复的劳动。每天的工作总会有一些事情需要简单重复的劳动。比如当你要验证一项功能时,你需要手工打开网页,然后按着流程一步一步操作,来看实现是否正确等。每当此时,我们的选择是:绝不做简单重复的劳动,而是实现一个自动化的脚本来帮助我们进行这些操作。自动化不同的任务,可以选择不同的工具,比如Selenium、Ruby、Shell等等。后来我在《The Productive Programmer》中读到Neal Ford大师如是说:“手工执行简单重复的任务会让你变傻,会消耗你的注意力,而注意力是最重要的生产力之源。找出一种聪明的方法来自动化这些任务,这会让你变得聪明,因为你能从中学到一些东西。”

每天早上的code diff:在早晨的站立会议结束后,我们并没有马上着手清扫story。所有开发人员会挤在一台机器前,查看前一天的code diff。大家会看到所有成员在前一天的工作中修改的代码,相关的开发人员会对自己的代码作出解释。当有人对一段修改有疑问时,我们就会对此段代码进行讨论。或者是实现上的一些逻辑漏洞,或者是一些不规范的代码编写,或者是一些可能的性能改进,我们总能对一些代码提出疑问,并提出改进意见。Code diff过程是对站立会议以及结对编程的补充,是对代码质量的进一步检验,有助于团队对代码的了解,也促进了代码质量的提升。短短十分钟,何乐而不为?

每周的技术session:每周的技术session是我们一直保持的良好传统,团队从中受益匪浅。项目中总会遇到一些难题,或许是一种陌生的技术,或许是一个难解的问题。而此时,总会有人站出来说:“让我来讲讲这个吧”。或许这项技术对于主持者也是完全陌生的,但不要紧,接下来一周紧张的学习已经足够(这也会促使他更加快速地学习)。一周之后,主持者就为我们带来了一道“丰盛的大餐”。在session中,团队成员都会踊跃发言,并乐于抛出任何疑问让大家讨论。讨论是最能产生火花的,来自每个人不同的思考会让你对问题了解得更加深刻。讨论帮助我们解开了一些困扰已久的疑问,并加深了对一些技术的理解,比如:CSS,Memory Cache, REST, Ajax,Ruby的对象模型等等。通过技术的讨论和学习,团队成员的整体开发能力得到了提升,这极大地促进了项目的开发速度。

团队需要勇于尝试一些实践来促进团队的开发效率和提升团队的整体能力。上面所提及的有些实践都是我们在平时的工作中摸索、体会和总结出来的。比如每周的技术session,就是在成功地开展了一次之后,被保留和坚持下来了。

如何发现一些值得尝试的实践?这看似很难,其实很简单。在项目中遇到的问题,工作中偶然发现的一些事情,别人的经验,或者是你自己的一些想法,都可能是对一项实践的启发。你唯一要做的,就是勇于尝试。“从来没有人这么做过”,或者“别人都不这么做”不是你不这么做的理由。

TimeMachine.go_to("2008-08-01")

当项目开发进入第5个月,我们遇到了一些真正的困难,它们分别是“历史”story和系统性能优化问题。

当项目遇到困难时,正是审视敏捷实践的最好机会。TDD,简单设计,持续集成,重构等等,让我们看看这些实践在项目开发中显现的力量吧。

笑看历史——关键字:简单设计,TDD(测试驱动设计)

历史story:作为全球质量管理员,我要查看一个酒店的标准审查历史记录,从而跟踪并比较酒店在各个时期的标准符合度。

简单分解一下这个story,它要求提供的功能是:

●  系统在一些比如标准变化、酒店审核等事件发生时能保存当时的数据。

●  系统应该提供一个界面,让用户通过选择特定的日期查看历史信息。

跟通常一样,我们经过estimation(当时估的是5个points,并不太大)以及tasking(简单的设计)之后,开始实现这个story。

刚开始,这个story如预期一样顺利开发下去了。一天,两天,到了第三天,我们突然发现在实现一些功能时有点举步维艰。但通过一些“邪恶”的手段,我们还是解决了那些问题,虽然觉得并不是最佳实现。到了第四天,在实现另一些功能时,我们陷入了绝境:实现得过于复杂让代码改动极其困难,通过了这边的测试,那边的测试就失败了,如此反复。还差两天迭代就要结束了,这个story却深陷泥沼,团队陷入担忧之中。我们决定重新跳出来看一下原来的设计。

基于实现中遇到的困难,我们讨论了原先的设计,发现一些缺陷,并迅速找到了更好的方案。新设计雏形初具,再次动手吧。改动代码,很多原先的测试应声失败了。好事情,失败的测试告诉我们哪里出了问题,过去修正那些失败的测试吧。红,绿,红,绿,按照这样的节奏,出乎所有人意料的事情发生了:一个下午,仅仅是一个下午,我们把历史story完成了!

因为这个设计并不在技术层面,而在业务层面,所以在这里我并不想多讲story的详情。但发生这样的问题,是否会让你怀疑敏捷实践“简单设计”呢?如果你有此怀疑,我的想法就正好跟你相反,因为通过这个story让我更加相信“简单设计”的好处。

首先,即使现在回头看,我们还是认为就算当初花几倍的时间去做设计,也难免犯同样的错误。其次,检验设计优劣的最佳工具就是代码本身,及早地应用设计于代码,让代码来告诉你设计正确与否。这正是在这个story陷入泥沼时我们的反应:实现不能继续,来看看设计的问题吧。再次,之后我们的设计之所以如此成功地符合了业务的需求,是因为前一次失败的经历让我们对代码有了更进一步的了解,对问题有了更清晰的洞察。谜团是在行进中解开的,并不是一开始就能知晓。最后,我想我们唯一应该改进的,就是在发现开发出现困难时,能更早地跳出来,从设计角度分析一下问题。

新的设计实现能在一个下午完成,是因为高测试覆盖率为正确的重构实施提供了安全保障。测试总能迅速地提醒我们哪些地方出了错,以保证重构的正确。其次,良好的代码结构,也是代码能如此快速地修改完成的原因。这些都是TDD带来的好处,TDD不仅能提供高测试覆盖率,也能带来良好的代码设计。这就是越来越多的人把TDD称之为测试驱动设计(Test-Driven Design)的原因。

TDD给设计带来的一些好处:

●  TDD迫使你在编写代码之前,考虑更多对象之间的交互。

●  TDD迫使你把对象的创建封装在一个更好的层次上。

●  TDD会让你写出更加小而内聚的方法,从而使方法的重用以及纠错变得更加方便、快速。

百倍加速——关键字:性能,重构,YAGNI

随着项目进入后期,功能的开发已经基本完成。此时,我们面临系统的性能问题:发布一个标准竟然要5个小时,令人无法忍受。由于发布标准需要在一个事务内完成,而且要修改大量数据。所以在这5个小时内,数据库的很多表都被锁定,系统几乎处于瘫痪状态。于是,顶着巨大的压力,我们开始了性能优化之旅。

先通过添加日志找出最耗时的操作,然后仔细跟踪和分析这块代码。我们随即发现了一个耗时十分钟的操作,这个方法的时间复杂度是n2。但这段代码犯了一个低级错误,它可以被优化成一个n复杂度的方法。立即修改,修改后由于消除了大量重复操作,它的耗时竟然不超过一秒。“秒杀十分钟”就这样诞生了。随着我们进一步的跟踪和分析,多处设计上的问题暴露出来。几天的改进之后, 5个小时的操作缩减到了1个小时。

但显然,一个小时的操作还是不能为客户所接受的。于是,继续优化。马上,我们就发现标准发布操作对很多标准都进行了重新算分,但实际上只需对有改动的标准算分即可。而没有改动的标准,可以把它的得分结果存于数据库,下次用到时从数据库读取即可。通过这个“缓存”,我们再次大大提升了速度。运行之,整个操作竟然只需5分钟!5个小时的操作,到现在的5分钟,百倍加速也!

这个性能优化问题是个典型案例,它给了我们很多启示。首先,在遇到性能问题时,先别忙着埋怨平台的速度,还是先看看设计里面存在的问题吧。其次,也是我们可以改进的一点是:在TDD“红-绿-重构”的标准开发节奏中,如果我们多花一点时间在“绿”之后进行重构,就可以避免犯一些低级错误。

或许有人会说,如果你在刚开始就做更多的设计,就能避免到最后出现这样的性能问题。但我想说,非也,你忘了:你将不需要它(YAGNI)。预想开发是个迷人的陷阱,或许可以在刚开始就花费十倍的时间去避免这个性能问题的产生。但问题时,在刚开始你怎么知道哪些是必要的设计,而哪些是浪费呢?敏捷团队注重的是给客户创造真正有价值的业务,而不是花费大量时间去制造一个“华而不实”的系统。

“只在确实需要时才提供功能”,这是我们遵循的准则。

最后,性能改进之所以如此顺利,得益于我们遵循的一些敏捷实践。比如TDD带来的高测试覆盖率,保证了重构的正确性。在BOSCO项目的开发中,不乏这样的大型重构,我们从不害怕修改代码,因为有坚实的测试代码为重构撑起了安全网。而反之,重构促进了代码结构的优化,提升了系统性能。

从这两个问题我们可以看到:

●  敏捷实践促进了系统设计的灵活,代码结构的优化,系统质量的提升。

●  各项敏捷实践之间其实是相辅相成,相互促进的。

BOSCO脚印

通过对BOSCO系统代码的统计数据,我们可以看到:

●  代码设计的优良:平均每个类不超过十个方法,每个方法不超过五行代码。如果除去特殊的Helpers模块,平均每个类不超过7个方法。

●  高测试覆盖率:测试代码与功能代码的比例是1.5:1,行覆盖率达到90%以上。

●  开发人员的熟练度:每对pair每天6次的提交速度足以证明开发团队对于系统的熟悉度。

由于Ruby On Rails是一个特别擅长开发Web应用的框架,考虑到其所具备的强大表现力,我们相信此系统的复杂度不亚于很多规模在十多万行代码的系统。

TimeMachine.go_to("2008-09-17")

时间定格在BOSCO项目第三次发布的那一天。除了需要一段时间的bug修复和数据准备之外,发布并没有给工作带来太多不同。我们从未因为发布的到来而惊慌,也从未在发布时忙得不可开交。正如团队一成员所说,每次我们都是在等待发布,安静地等待着发布的到来。因为合理的安排和控制,让一切尽在掌握中。

发布成功,欢呼之余,让我们回头看看项目中的哪些实践保障了发布的顺利完成吧。

客户协作——关键字:PM、BA,还有Dev

曾经有一个同事突发感想说:“在ThoughtWorks做程序员是幸福的”。这不禁让我有很多感触,是的,在ThoughtWorks做程序开发是一件幸福的事情。当你有任何关于业务逻辑上的疑问时,坐在身旁、招之即来的BA总能给你准确地解答;当你坐着投入地进行开发时,可能你从未想过,之所以有这么良好的环境,是因为PM帮你阻挡了一切不该有的干扰。

PM和BA在背后付出的需求分析、客户交流等工作,让开发人员能把大多数时间和精力花在编码上。

BA在每天的站立会议之后,会有一个跟客户之间的BA站立会议。在这个会议上,他们会讨论各个业务的细节,力保每个业务细节的正确性。正是业务的正确性,让每天的开发工作能顺利进行。

PM每天会用很多时间跟客户交流项目进度,确保客户了解项目状态。并在每天工作结束之后总结一封报告邮件,这封邮件包含了对一天工作的总结,各个story的进度情况等。通过这封邮件,客户能及时地了解进度情况。

那Dev能在客户协作方面贡献什么呢?有人觉得,开发人员的任务就是从BA那里拿到story,并严格按照需求开发,而无需做与客户协作沟通等工作。其实这并不准确,Dev完全可以在客户协作方面发挥自己的作用,因为对于整个系统架构的了解,Dev有时候比BA,甚至比客户自己还了解他们真正需要的是什么。当我们拿到需求时,我们可以从开发者,以及整体系统架构设计者的角度考虑一下需求的可行性、必要性。有时我们会直觉有些需求并不是客户真正想要的,或者我们可以通过另外更简单的方式给客户提供同样的功能。经过一些分析,以及跟BA的讨论之后,BA通常会接受我们的观点并与客户进一步讨论。在这个过程中,Dev起到了帮助客户认清真正需求的作用。这是一个双赢的结局,开发人员不需要为一些不必要的功能而增加不必要的工作,影响架构的稳定性;同时,客户可以不必为一些价值不高,甚至没有价值的功能而付出昂贵的代价。

所以,客户协作并不只是PM和BA的工作,Dev作为系统的开发者,应该从他们的角度帮助客户找到对客户真正有价值的业务。

同时,信任,是客户协作的基础。团队与客户之间只有真正信任了,才能更好地合作。我们和客户之间的相处,就如朋友,在平时的工作中甚至会经常拿对方开开玩笑。

团队合作——关键字:Retrospective,Feedback,持续改进

大家都说,这是我们呆过最开心的一个团队,因为从不缺少欢声笑语。而我觉得,这更是一个正直的团队。无论谁有优异的表现,我们从不吝啬赞扬;无论谁犯错时,我们也会毫不犹豫地指出。

每两周一次的retrospective提供了一个寻找团队问题的好机会,在每次的回顾反省中,我们都会找出一些项目中的问题,并在接下来的工作中给予改进。这样团队才能保持持续的进步。

BOSCO敏捷 图注:Retrospective有助于团队总结经验,持续改进

有人会问,为什么在我所在的团队中就不能保持这样良好的气氛,无法保持正直的态度呢?究其深层次的原因,首先需要公司的制度让团队的成员处于平等的地位,不应该有谁是在“管理”谁。PM,BA,Dev之所以需要不同的职位,只是因为工作类别不同而已。PM负责管理客户期望,BA负责需求分析,而Dev负责的是项目实现。

其次,团队作为一个整体,不应为任何问题追究到个人,而应把它归为团队集体的责任。平等的地位和责任集体所有制,会让每个成员更具主人翁精神,也会让团队更加紧密地凝聚在一起。团队成员的相互信任和紧密合作是项目成功的根基。

TimeMachine.go_to(“Future”)

敏捷方法并没有高深的理论,有的只是一些简单的实践。正是这些简单的实践,提升了开发人员的效率,促进了项目质量的提升,保证了项目最后的成功。

在未来的路上,让我们一起寻找更好的敏捷实践吧。

Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐