• 作为一个有理想、有追求的程序员,你成天被各种名词包围着,你对其中一个叫做敏捷的东西特别感兴趣,因为它特别强调人的作用,这听着都让做程序员的你感到舒服。为了让自己早日敏捷起来,你从众多的敏捷实践中选择了一个叫做测试驱动开发(Test Driven Development,TDD)的作为你的起始点。因为它对你周遭的环境要求是最低的:它不像结对那样,要求其他人和你一起合作;也不像采用Story那样改变你所在团队的做事方式……你所需要做的,只是在你编写业务代码之前,把测试先写好。这完全是一种润物细无声的做法,根本无需告诉你之外的任何人。就在别人忙碌的找bug时,你便开始享受敏捷带给你的快乐了。顺便带来的好处是,下次在那里和别人争论敏捷的时候,你可以以一个实践者的姿态出现,而不是在那里信口开河。

    你不会打无准备之仗,于是,你通读了Kent Beck的那本薄册子。通读之下,你对TDD更是充满了信心。因为“红——绿——重构”的步骤实在是简单得令人发指。好吧!总而言之,你已经信心十足的准备开始TDD,步入敏捷的康庄大道了。

    理想很美好,现实很残酷。

    当你着手在实际项目中体验TDD的时候,一切变得并不像最初看起来的那样美好。虽然你努力的坚持着TDD的原则,但你经常就会发现某些东西不好测,比如你遇到了数据库,比如你遇到了GUI,比如你遇到了计时器(Timer)。敏捷并非教条,当某些事不可为的时候,你完全可以不那么坚持。于是,你告诉自己,不好测的东西可以不测,这样,至少从心理上来说,你觉得舒服多了。随着工作的继续,你发现,你不能测的东西越来越多,单元测试的覆盖率随着开发的进行正在逐渐降低,一丝恐惧涌上心头。回过头来,再去看Kent Beck的书,你突然觉得,你似乎被骗了,因为Kent Beck的例子貌似全都是逻辑,如果只是逻辑,当然好测了,但现实从来就不是这样。

    难道TDD只是看上去很美?

    显然,你不愿意就这样放弃,放弃你苦心学来的软件开发秘籍,那些传说中的高手极力推崇的TDD必然有一定道理,TDD确实能够让你感觉很好:能测试的那部分代码确实极大的增强了你对软件质量的信心,而且出错了也确实好找,每次修改代码之后运行测试出现的绿条也确实让你身心愉悦。

    那问题到底出在哪呢?你陷入了沉思。

    信马由缰,你翻开了自己写过的代码。看着自己写的这些代码,你忽然意识到一个问题,自己遇到的问题并不属于TDD,而是属于单元测试。正如你之前所想到的那样,TDD做法本身的结果是让你感到快乐的。对,一定是单元测试本身出了问题。那单元测试出了什么问题,很显然,一大堆不能测试的部分让单元测试变得很难写,降低了单元测试的覆盖度。那是不是这会是一个无解的问题呢?你显然不愿意就此放弃,所以,顺着这个思路继续向前。

    TDD之所以让你安心,主要是每次编写代码之后,运行测试会出现一个绿条,告诉你测试通过。这样,你可以放心大胆的向前继续,因为你的代码并没有破坏任何东西。究竟是什么让你感到不安,显然是那些测试没有覆盖到的代码。你又仔细翻看了一下那些没有测试覆盖的代码,你的思路一下子清晰起来。之所以这部分让你不安,因为里面除了那些确实不好测试的部分之外,里面还有一些逻辑。如果只是那些真正不好测试的部分没有被测试覆盖到,你会觉得心里还有一些安慰。你确定了,真正使你不安的就是与不好测试的代码共存亡的这些逻辑部分。

    如果测试可以覆盖到这些逻辑的部分,至少从感情上来说,就可以接受了。那怎么才能让这些部分被测试覆盖到呢?你仔细观察着那些没有测试的代码,如果这样做,这个部分就可以测试了,如果那样做,那个部分也可以测试了,一来二去,这些貌似不可测试的代码可以分解出许多可以测试的部分。

    你的心情一下子好了许多,因为这么做终于可以让测试的覆盖度达到让你心理上可以接受的范围。不过,新的问题也随之而来。我在做什么?拆来分去,这不就是设计吗?怎么走到这里来了。我不是在分析单元测试的问题吗?对了,我最初的问题是TDD,怎么一路跑到设计上来了?

    TDD?设计?

    你突然发觉自己对TDD的理解有一些偏差。TDD,并不代表不需要设计。读过很多书的你突然想起了Robert Martin那本著名的《敏捷软件开发》,上面有一个关于数据库访问的例子。那个例子里面,前后两个版本的差异正好就是考虑设计的结果。通常,在设计中考虑测试,会很容易找到设计中僵硬的部分,让程序更加灵活。再进一步,如果在开始动手之前,稍微进行一些设计,这些问题还是可能注意得到的。你突然觉得,正是因为TDD本身过于强调测试的价值所在,让你忽略软件开发中很重要的部分:设计。

    思路一下子清楚起来,TDD其实不只是“红——绿——重构”,它还是与设计相关的:在动手之前,还是要有一定的设计,而且,在设计中要考虑测试的问题。终于解开了心中的困惑,现在的你,对于TDD有了一个新的认识,虽然这个认识不见得是什么终极真理,但至少是通过自己的思考得来的,这让你更加相信实践出真知的道理。

    理清思路后,你更加坚信TDD本身的价值所在,也坚定了在日后开发中继续使用TDD的念头,当然,目光远大的你已经盯上了其它的敏捷实践。

  • 2007-12-25

    圣诞聊敏捷

    在ThoughtWorks待了大半年,听的见的经历的最多的当然是敏捷。圣诞之夜,不妨整理一下现时心目中的敏捷。

    提及敏捷,主要是两种反映,济世良药或是洪水猛兽,当然,也有人置身事外。其实,敏捷只不过是一种软件开发方法而已,与传统软件开发方法开发方法并无本质区别:敏捷也需要知道到底软件要干什么,所以,分析需求不可或缺;敏捷最终也是把软件提交给客户,所以,也要一行行把代码写出来;敏捷出的软件也会有错误,所以,测试阶段是不可缺少的;敏捷出的软件业务也会变化,所以,代码也需要维护;敏捷的情况下,为了在给客户展示,所以,偶尔也要加班……

    如此说来,敏捷是不是和传统软件没有分别了呢?显然不是,任何事物一旦拉高到本质的高度,恐怕都没有区别了,就像“人”可以涵盖人这个物种,但事实上,人还有男人、女人、好人、坏人……等等众多区别手段。

    相信大多数人有这样的感觉,念书的时候,总有那么一些怪物,看起来,玩得不比别人少,书读得不比别人多,但考试成绩总比别人。这样的怪物是怎样炼成的呢?说起来,很简单,他们懂得了一些学习的方法,所以,他们可以在同样的时间内,取得比别人高许多的结果。

    做事,是有方法的。

    好的做事方法绝对是大幅度的提高效率的。相对来说,敏捷就是一种好的工作方法。

    敏捷重视人在软件开发中的价值,所以,在敏捷团队中,开发者在团队中地位很高,这让我们在心理上得到了极大的满足,于是乎,心情很是愉悦。在很多强调传统软件工程的团队中,开发者不过是一颗螺丝钉,一个可有可无的角色,受重视程度自然很一般,除了自力更生,心情上与敏捷团队不可同日而语。两种团队,我都经历过,差异极大。其实,无论在什么样的团队中,我们都要完成类似的工作——编写代码,但是,想必人人都清楚,心情对于一个人工作的影响。

    除了完成工作,我们需要的还有发展。专业技能上的成长,除了自己摸索,更重要的还有吸收来自别人的营养。在传统的方法团队中,除了读书,只有偶尔大家的交流才能让人得到向别人学习的机会,大多数情况下,我们所能做的就是单枪匹马的孤军奋战。幸运的话,也许能够走上的正确的道路,大多数人必然会绕很大的圈子,更不幸者,也许就此失去了对软件开发的兴趣,或默默忍受编程带给自己的痛苦,或干脆离开这个是非圈。

    在敏捷团队中,过程本身采用的一些方法就是前人经验的总结,比如测试驱动开发。结对编程可以让人有机会观察别人是如何工作的,从中吸取养分,这些细节是几乎不可能从书本或是课堂学到的,比如,在ThoughtWorks工作之前,我从未意识到快捷键对工作的帮助如此之大,当看到其他人按键如飞后,我决定在今后的开发中,尽可能多的使用快捷键。在两个人共同开发的过程中,我们有机会看到自己同伴对于一个问题思考的过程,这对于我们的提高绝对是有益的,特别是和一些高水平的人合作。良性循环和恶性循环显然不是一回事。

    软件是为人服务的。软件开发是为了将软件创造出来,而不是为了为难开发团队。如果你曾经在传统开发团队工作过,提到这一点,脑子中多半会浮现出“文档”这两个字。敏捷团队也会写文档,但我们写的是那些对客户有价值的文档,而不是把时间浪费在那些为了过级而编写而且完成之后便会束之高阁的文档。不给自己找麻烦是很必要的。

    敏捷和某些传统软件开发过程可以说是殊途同归,比如,CMM到了最高的级别,会向自适应的方向发展,而敏捷本身也是在过程中不断的进行自我调整,做到兵来将挡。当然,走到一起之前,二者的差别显得还是很大。

    以上这些都是站在我——一个开发者——的角度体会的敏捷,显然,并不全面。某些人提到敏捷,还会有更加伟大的角度,比如,业务之类的,那不是我所擅长的。

    既然敏捷如此之好,为什么敏捷团队还是如此有限?回到最初那个读书的比喻上,优等生的方法不见得是每个人都知道的。即便知道了,相信、学习到掌握还要有一个过程。敏捷是不是终极解决方案,我想不会,天外总有天,如果哪一天,我知道更好的方法,我愿意接受。

    写下这些文字的时候,Ruby 1.9.0发布了,感谢Ruby开发团队带给大家的圣诞礼物!

  • 原文:
    http://blogs.msdn.com/jaybaz_ms/archive/2007/11/09/parting-words-for-dear-friends.aspx

    InfoQ的报道:
    http://www.infoq.com/news/2007/11/criticism-from-microsoft-devlead

    InfoQ China的报道:
    http://www.infoq.com/cn/news/2007/11/criticism-from-microsoft-devlead

    有一段时间,我一直想不明白一件事,软件开发中,是算法和数据结构那些实现技能更重要,还是软件设计和编写干净代码的能力更重要。前者的代表应该是微软和Google,后者的代表应该是ThoughtWorks。公司的侧重,在这些公司的招聘过程中,体现得尤为明显。

    所以,我们经常看到一群人为了证明某一个方面的重要性,举出无数的理由试图证明另一方面不重要,这样的争论有如语言之争一样,没有任何一方可以说服另一方,结果便是一群人争论半天,回去各自做各自的事去了。实际上,甚至可以说两者都不重要,因为有为数不少的程序,是在没有太多技术难度,也深入考虑设计和架构的情况下完成的。这也是很多人抱怨自己的工作没有技术含量的原因。

    在我看来,两者都很重要。

    过分强调某一个方面,导致的结果就是另一方面的欠缺,这一点尤其容易出现在有着很强实现本领的人身上。当我们说某人很牛时,多半指的是他能实现出一些别人无法轻易实现的东西,显然,没有人愿意称一个只会写“Hello, World”的人为牛人。所以,一旦不小心被当成了牛,很容易就真的以为自己是牛了,所以,忽略了另一个方面,对于工程很重要的方面。

    有一种说法是,某人牛到写出的代码别人看不懂,这一方面是这个人实现本领过强,另一方面也说明,这个家伙着实不好合作。当然,某些算法初读起来确实令人费解,但大多数情况不是这样。我着实见过一个被周围的人当作牛的人写的代码,一份让我觉得愤慨的代码,除了实现功能之外,一无是处。在工程中,设计出良好的结构,编写出很好的代码,对后续的开发和维护都是一种重要的基础。当某人因为点灯熬油成功修复一个bug而受到大家吹捧的时候,请别忘了,多半这个bug就是因为他自己没有良好工作习惯而埋下的。

    微软开发主管的临别赠言为这种情况添加了一些注脚,微软是一个大家认为的牛人集合体,出现这种情况,只能说明,他们软件开发的另一面重视不够。

    反方向的情况似乎并不明显,没有人会因为自己有良好的设计功底而忽略了自己的实现技能,毕竟,有高超实现技能的人,才是“牛人”。

    不过,强调实现技能这点也有好处的。随着微软和Google对实现技能重视的宣传,大家越来越重视这些基础的东西,这让很多人,尤其是还没有明确自己努力方向的学生们看到了一丝曙光,因为这至少不会让他们一无是处。

    我一直喜欢程序设计是一种艺术的说法,所谓艺术,必然会有一些美的东西在里面。无论是实现了一个精巧的算法,还是搭建出一个宏伟的架构,这些无不是体现程序设计之美的地方。想要体会程序设计之美,那就势必要两手抓,且争取两手都要硬。
  • 作为一个程序员,获取知识是让我不断前进的动力,而读书是我获取知识的一条重要途径。在这个“经典”、“必读”过剩的年代里,大多数的书都仅仅扮演着传播知识的角色,真正改变自己对某些问题看法的书其实少之有少。限于读书时的眼界和能力,在我列表中,让我拍案惊奇的书只有几本。Martin Fowler的《重构》,严格说来,我并没有完整的读完这本书,不过,正如作者自己所说,这样的书原本就不指望能够读完,因为有一大部分其实是参考手册。正是我读过的部分让我知道了重构,让我知道这么做可以把代码写得更好。Robert Martin的《敏捷软件开发》,这是一本名字赶潮流,内容很丰富的书,这本书让我开始理解软件设计,从此不再刻意追求设计模式。Kent Beck的《测试驱动开发》,我读的是英文版,因为当时中文版还没有出版,所以,我不敢说,我通过这本书很好的理解了测试驱动开发,但它却为我打开了一扇门,让我知道了一种更好的工作方式。

    有好长一段时间,这个列表没再更新过,中间虽然我也读了很多书,也学到了很多东西,但却没有哪本书如这几本书一样给我带来巨大触动。新近加入我这个列表的书是《修改代码的艺术》,英文名是《Working Effectively with Legacy Code》。

    对于很多软件开发人员来说,加入一个公司,通常意味要面对一大堆之前留下的代码。而面对沉重的负担,大多数人的感觉都是无可奈何。让无奈成为往事,也就是这本书的价值所在。

    在我看来,这是一本讲解如何编写测试的书。之所以遗留代码让人头痛,除了复杂的逻辑,改动会带来怎样的后果是一件让人心里没底的事,而测试的存在可以大幅度降低这种恐惧。但是,许多代码在开发时并不考虑测试,这样做的结果就是让测试几乎成为一件不可能完成的任务,一个常见的例子就是代码中访问数据库。即便写出测试代码,漫长的测试过程也会让它失去一部分应有的作用,我们希望得到的是快速的反馈。所以,对于
    无测试而言,知道编写测试是一种境界的提升,写好单元测试则是一种更高的境界。如果能够让测试驱动开发,从开发之初便考虑测试,并懂得如何写好测试,开发者应该不会陷自己于一种难为的境地,这也应该成为专业程序员应该具备的基本技能。

    至于这本书的具体内容,我的评价是实用。具体的手法,很难在这里一一列举,但是,以我的开发经验来看,
    许多似曾相识的代码不断的出现在书中,而作者举重若轻的处理手法,正是让我有拍案惊奇的地方。实际上,回味起来,每个手法都不是什么很高超的技法,但正是因为见识过类似的代码,才能体会到这种手法的价值所在。所以,相对于程序新人,它更适合有经验的人。

    之所以说这本书更适合有经验的人还因为,这本书中谈及的内容涵盖设计、测试、重构等诸多方面:通过重构,解开代码内的耦合,让其可测。这恰恰是前面提到的那三本书所讲的内容。也只有懂得了这些基本内容才能体会到那些具体手法的价值所在。依然记得当年读《重构》时,在提取和内联之间迷茫了好久,直到后来经过了许多开发实践才体会到这些做法的真正含义。

    如果说不足,那么,这本书缺乏一个列表,就像Martin Fowler为《重构》所做的那样,出什么样的问题,应该采用怎样的手法进行处理。

    关于中译本,总的来说,翻译得很流畅,读起来比较舒服。不过,制作上还是有一些不太让人满意的地方。
    * 译注太多,而且有些是低估读者智商的译注。
    * 页边标有页码,似乎是为了与英文版对照,但文中的参考页码又是以中文版为准,显得有些乱。
    * 书的装订不是特别令人满意,我一直担心从中间断开。
  • 今天下午,和Darwin聊了一下C++单元测试框架,主要参考对象是CppUnit和CxxTest。

    表现形式
    因为C++不支持reflection,所以,必须要做一些额外的工作,让框架知道相关内容的存在。CppUnit的做法是用宏进行注册。这种做法要求我们每添加一个测试,就要考虑用相应的宏进行注册,这种做法很繁琐,最大的问题在于由于疏忽而遗漏,这种靠人工保证的东西不可靠。在这点上,CxxTest做得要好一些,有一个专门的脚本做这件事。通过这个脚本扫描这个自己编写的文件,生成一些新的文件,完成这个工作。从代码的表现力和可靠度来说,要好得多。唯一的问题是引入了一个脚本,而且这个脚本一般是由某些动态语言写成的(目前的CxxTest有Perl和Python的脚本),从而引入了对这种语言的依赖。不过,由于C++语言本身的限制,从接口的角度来看,这种做法已经很不错了。

    语法
    有一种C++的单元测试框架叫TUT,Template Unit Test的缩写。顾名思义,它是用模板完成的(其实,CppUnit和CxxTest都有模板的部分)。随着C++编译器的进步,在大多数情况下,模板都是可以顺利通过编译的。但是,不要忘了,还有一种环境叫嵌入式,那里的编译器基本上还是很原始的,模板并不见得能够顺利的通过编译。

    此外,模板还会带来另外一个问题,编译时间的增长,相信有过模板编程经验的人都会对此深有体会。编译时间增长意味着什么?我们接下来讨论。

    编译时间
    有一种敏捷实践叫做测试驱动开发(Test Driven Development)。测试驱动开发的基础是单元测试。测试驱动开发希望达成的一个目标是快速反馈,所以,站在C++语言的角度,如果执行时间受限于代码本身无法缩短,那么我们希望编译时间尽可能短,这样,才不会把生命都浪费在等待代码编译上。

    除了刚才提到的模板问题之外,CppUnit会把所有测试编译生成一个可执行文件,这意味着什么?几乎修改任何一个文件都会造成这个文件的重新生成。随着目标文件的增加,这个过程时间就会增长。相对于修改范围(可能只是某一个文件),还是显得有些长了。为什么Java语言不会存在这种现象?因为Java是动态连接的,所以,Java生成.class就结束了。对应到C++上,这只是完成了目标文件的生成,而在C++我们不得不再进一步生成可执行文件。从道理上,CxxTest可以为不同的测试文件生成不同的可执行文件,不过这么做又少了总体的过程,统计起来又显得心有余力不足了,而且通常不会这么做。

    个人而言,对这几个单元测试框架都不是非常了解,如果前面的讨论存在谬误,欢迎有识之士指出。