代码改变世界

我眼中的敏捷实践

2011-02-27 08:50  横刀天笑  阅读(3444)  评论(8编辑  收藏

最近coolshell.cn上有几篇对TDD以及ThoughtWorks颇有微词的文章,然后园子里的Todd Wei同学也来了篇TDD到底美还是不美的文章。本来不想趟这个浑水,但想来想去还是有些话想说说。

声明:本文我不想议论ThoughtWorks怎么样,要我说她的坏话这有点过不去,她是我的东家。要我说好话,大家也肯定不信,说我自吹自擂。我只是想说,如果你想了解ThoughtWorks,可以多去接触一些ThoughtWorks人。是的,我说的是多接触一些,比如你不要看到我是个菜鸟就说ThoughtWorks怎样。ThoughtWorks北京办公室是OpenParty的场地赞助,可以参加一下去了解了解,你也可以发私信给我。所以,我要声明的是本文和我所在的公司无关,只代表本人观点,也希望在评论中不要出现相关言辞,我会全部删除的。

好了,还是进入本文的主题。

首先请各位看看本文标题中的敏捷实践。为什么说是实践而不是说什么方法论,什么模式呢?我的看法就是实践能够确确实实落到实处。很多人说软件开发是一门艺术。但是艺术这个东西虚无缥缈,那要开发出好的软件还要培养出我们的艺术细胞,实在是不好操作。所以我们就需要一些能够落到实处的实践。这些实践很好操作,没有什么模棱两可的东西,也没有什么故作高深的东西,更没有什么只可意会不可言传的内容。

比如,在我前不久写的一篇文章《或许你需要一些可操作性更强的实践》中说到,面向对象是个很抽象的东西,所以很多书都以各种小猫小狗的例子来教导我们如何去以面向对象的方式思考,这些例子在阅读的时候都朗朗上口,但是一到我们实际的项目中又全部玩完。所以我们需要一些看似有点死的实践:我们规定类和方法的行数,规定不要出现重复代码,规定一行不要出现多个“.”操作符等。这些都可以称之为实践,因为它非常好操作,一眼就可以看出来问题。

那有人可能要说了,软件开发是一门艺术啊,你这样搞和建筑队有啥差别。嗯,是的,这样弄起来可能有点土。但是我突然想起来,以前老师教我们画画的时候,不也是介绍了一些类似的实践么,比如画人像的时候不是有个三停五眼么(如下图)。画画是艺术吧,但是还是会采用一些看似死板的方法。那么,我们软件开发为什么就不能使用呢。

image

我想说的就是,敏捷实践就像这个三停五眼,他很好落实。测试驱动开发就是这些实践中极其重要的一个:

1、程序员都是技术人员,常常沉迷于技术细节而不能自拔,忘记了自己所要开发的软件的业务价值,有的时候甚至自己想出很多自以为对用户有用的功能来,但实际上交付给客户后,客户发现添加的这些东西根本一文不值。而测试驱动开发则用一种有点极端的方法来规定:测试通过了,你不要再写代码了,除非你添加一个失败的测试,否则我拒绝添加新的代码。以此来防止我们过分的追求技术,而忘记了业务价值。

记得很早之前,我的经理经常跟我说,你太过于关注技术细节,而忘记了一些很重要的东西。我那个时候不以为然,后来每次想起来就觉得惭愧。比如我经常为了构造非常好的类阶层次结构,将代码改来改去,有的时候为了实现这个目标甚至自行消减了部分软件功能。

2、设计是非常困难的一件事情。我们一直在强调,要高内聚,低耦合。那么我们又怎样构造出高内聚低耦合呢?靠喊口号当然不行。而如果你一开始就使用测试驱动出产品代码的话那么创建高内聚低耦合的代码就更容易得多。

比如上周,我在给一个遗留代码添加新的功能。具体需求大概是这个样子:根据当前用户以及所访问的内容,然后其他管理员设置的策略来显示签名。因为要将代码打包部署之后才能看到效果,而且这个遗留代码非常庞大,重新部署之后tomcat基本上要五分钟左右才能反应过来(每每这个时候我就很怀念IIS)。所以手工的测试显然是非常不合理的,每次打包部署都要浪费这么长时间。而且我们还修改了遗留代码,必然要进行回归测试。没办法,我们要加个集成测试,这样就免去了打包部署的步骤了。我们大概花了一上午的时间将这部分集成测试编写完成可以跑了,但最后总是出现异常。最后发现异常发生在一个叫UserRightBRO的类上,它有个currentUser方法,聪明的你可能已经想到了,这个currentUser当然是web上下文中的一个东西,我们这个集成测试里怎么可能获得这个东西呢。解决这个问题最好的办法当然是编写一个派生自UserRightBRO的类,然后将currentUser方法用我们的实现覆盖掉,然后在测试时使用这个子类替换掉原来的类。最后我们绝望了,UserRightBRO这个类是在调用它的类里实例化的,而不是通过接口传入的,这两个类紧紧耦合在一起。

可能你要说,编写这个地方的人就是个傻蛋,搞出这么紧耦合的东西。但是,你用什么办法保证我们自己就不会编写出这么高耦合的类么?敢打包票么?当然不敢。但我想如果这部分代码是用测试驱动出来的(这块是遗留代码,啥测试都没有),我觉得应该会防止这个问题的出现。因为你编写测试时,就会准备一些基础构造,当准备的时候你就在想我们应该如何使用这个类,需要一些什么东西,像这个对环境依赖这么强的实现基本上不可能出现。

3、你说我们该不该写测试?如果我们开发的产品不发布或者就使用一次,或者发布给内部用户用用(他们一般能忍受你),那你大可以不写测试。什么?你说你们有庞大的手工测试团队?好吧,我认了。如果没有庞大的手工测试团队,那我们最好还是多写一些能够自动化的测试,但是但是但是,我连说了三个但是。多少开发人员在代码编写好后又回来补测试?这样做的请举手。从我了解的情况,我觉得不多吧,那如何让测试落实到实处?我想,在编写产品代码之前先写测试也许是一个不错的方法。

关于TDD可以将一些难以操作的东西落实到实处的观点还可以列出很多,不过要声明的一点,我这里想说的是我不是在为TDD唱赞歌。因为一些敏捷实践将很抽象的问题具体化了,将一些不好拿捏的问题具体化了,将一些看似高深的技巧简单化了,所以也难免会遇到一些问题,所以在实践中还是会有很多阻碍的。

比如Todd Wei在他的文章里所描述的,当需求改变时我们的测试会红一大片,甚至会抛弃很多测试。其实我想回答的是:

1、即使我们不测试驱动开发,我们也应该编写测试是不是?那么这个时候如果需求变了我们改不改测试?不过Todd最后说,我们要快速的构建出可工作的软件,然后让用户看看,然后等需求稳定了我们再编写测试。好吧,如果这一条真的有效我想你是不需要这些敏捷实践了,因为你已经将敏捷运用到极致:频繁的沟通和反馈。频繁的沟通和反馈可以有效的抑制其他风险。

2、我们为什么不能用测试反应出需求的变化?如果用户改变了需求,我们先改变我们的测试,然后修改我们的功能代码,这样也没什么不可。

3、我最想说的是,其实需求的变化频繁是针对整个软件系统的。当落实到具体的某一个类,需求变化就不频繁了,甚至不会变化。这是一个粒度的问题。

4、不要简简单单的拿出TDD出来讨论,敏捷的很多实践是联系在一起的。我们拿到一个软件需求,会根据用户划定的优先级决定先开发哪些功能。然后我们再将这些功能划分为一个个粒度更小的用户故事,这些用户故事的粒度非常小,在可见的将来他的需求甚至都不会变化,用户故事上还有验收条件,这些都非常具体。当然,这些功能列表,用户故事的划分,验收条件都是有业务分析师和客户一起划分的,不是我们开发人员臆测。所以,在测试驱动开发时,我们用测试反应客户需求绝不是随便瞎扯,因为用户故事粒度很小,所以就会描述得很具体,只要你认真的读一下用户故事这部分需求我想你是搞不错的。

在Todd的文章中,他频繁的提到我们要尽快的构造出可工作的软件,然后提供给用户,然后等着用户提供反馈。

这个我举双手赞成,没问题,我们就是要频繁反馈。但是反馈是有代价的,在软件开发中有许多反馈环,比如开发人员到开发人员,开发人员到QA,开发人员到业务人员,开发人员到客户等等。一般来讲,内环的代价最低,而自动化的测试就是这么一个反馈环,我们编写一个测试,然后编写功能代码,然后测试失败,然后我们写代码让测试通过。我们就是在这么一个周而复始的小反馈循环下工作,然后蔓延到更外的反馈环。

然后我们来描述一下我们实际的工作情况,怎么来解决这个反馈的问题:

前面的划分用户故事就不说了。我们在每完成一个用户故事之后就会做一次mini showcase,这个时候我们会邀请业务分析师,QA以及其他对这个故事感兴趣的项目组成员参与,这个show很小很短,只要在开发机器上演示一下,然后业务分析人员和QA人员会对一些他们关切点提若干问题,如果这个show通过后QA会将故事卡从正在开发移动到测试阶段。我们有一个迭代周期,为两周。两周后QA和业务分析人员一起会将本迭代周期已经测试好的用户故事收集起来,然后当着客户的面进行演示,然后收集客户的反馈。这个迭代周期也非常重要,如果你们有很合作的客户,而你们的软件属于那种需求变更非常频繁的类型,你大可以缩短这个迭代周期,比如一周。但还是那句话,越外环的反馈需要更大的成本,所以这是一个权衡的问题,我们需要尝试,然后才能确定。

敏捷里非常强调反馈,但是过频繁的反馈代价非常大。首先客户不一定会和你一起工作,即使和你一起工作频繁的将不成熟的软件演示给客户影响也不是太好。但是长时间不演示给客户看,就很有可能偏离预定的轨道。所以我们要一个反馈周期,要一个个反馈代价递增的反馈环。

文章写到这里,都有点乱了,其实我还有很多话要说。感兴趣的同学可以移步到coolshell.cn看看这篇文章[转]TDD到底美还是不美?下面jnj和陈皓的评论。内容非常精彩。

后记

在测试驱动开发中有一个大忌就是在写一个测试,或通过一个测试的时候想得太多。就像陈皓所说,WoW是一个非常庞大的软件系统,他有xxx,xxx,xxx,xxx。是的,我都承认,非常庞大,非常庞大。但是再怎么庞大的系统不也是一行行代码编写起来的么?我在编写怪兽移动的代码的时候我为啥要考虑任务系统呢?我们不可能一口吃一个胖子,在做TDD的时候你就做好TDD,其他的事情有其他的环节在考虑呢。好吧,那我不测试驱动了,那我到底写不写测试啊?如果你觉得编写测试是有必要的,那么所有TDD会遇到问题都会出现而且会来得更猛烈。所以,测试驱动或许还能减少一些我们的痛苦。

当然,如果你的功力已经到了一定的层次,对软件的需求和整体架构也非常了解。有些敏捷实践确实让你觉得太走极端了,那只是说明你已经超越了这些实践,你已经将这些实践当作一种习惯了,你大可抛弃就是了。比如这里有个很好的例子就是老赵写的两篇文章:

我的TDD实践:可测试性驱动开发(上)

我的TDD实践:可测试性驱动开发(下)

老赵的这两篇文章虽然没有采用传统的Test First的TDD,不过看得出他很强调“可测试性”。“可测试性”虽然看起来简单,但要达到这个目的就跟可扩展性,可靠性等其他软件非功能质量一样,非常难以达到。不过如果你出现的情况跟老赵一样,一拿到需求设计就清晰可见,而且还是能构造出松耦合的程序,那么你大可抛弃TDD就是了。不过你还要问问自己,你会不会像老赵一样最后会补上单元测试。

总之,是好是坏自己尝试一下吧,到网上找点相关文章和示例先看看练习练习,然后在自己的项目中试验试验,如果还是不行就查找一下自身的原因,如果你最后还是发现这玩意儿不适合我自己,那么抛弃便是。