今年春节时,我写了一篇《TDD并不是看上去的那么美》,在这篇文章中我列举了一些关于使用TDD的一些难点和对TDD的质疑,后来出现了一些争论(可参见那篇文章的评论),以及Todd同学的《TDD到底美不美》,还有infoQ中文上的那个几乎没有营养离线讨论。今天,有网友给我推来一个英文版infoQ的视频——“Coplien and Martin Debate TDD, CDD and Professionalism”,这是2008年2月18日的视频,视频的主角两个人争论TDD好还是不好,一个是敏捷社区的教主级的人物——Robert Martin(大家称之为“Bob大叔”),另一个是C++,OO,多范式编程的大师Jim Coplien(大家都叫他Cope)。这两个人对TDD的见解有分歧。Coplien的很多观点和我之前的不谋而合,而他自己称他是坚决强烈地站在TDD的对立面上。下面是Jim的原话:

I have adopted a very strong position against what particularly the XP community is calling test driven development.

InfoQ的视频很多时候相当的不给力,就像有前列腺的患者撒尿一样,半天都挤不出一滴。不过,好在那里有这两个人对话的摘录。在这里,我给大家摘要一下:

——————————————————正文分割线————————————————————

Coplien首先让Uncle Bob定义了一下TDD,Uncle Bob说明了他的三个法则:(敏捷的同学一定不陌生)

  1. 一个测试驱动的程序员,其不会在写出一个测试失败的Unit Test前,去写一句可用在生产线上的代码。(没有测试之前不要写任何功能代码)
  2. 在编写用于生产线上代码之前,不写过多的测试失败的Unit Test。(只编写刚好能体现一个失败情况的测试代码)
  3. 在现有代码通过Unit Test前,不写更多的用于生产线上的代码。(只编写恰好能通过测试的功能代码)

Coplien说他有意见的不是这三个法则,而是因为这个三个法则是孤立说出来的。Coplien说他和一些咨询师或是Scrum Master参与过很多的项目,他们发现这些项目都有两个问题:

  1. 他们使用TDD的时候,软件没有一个架构或是framework。当然,Kent Beck说——TDD可以驱使你去做架构。但是,TDD和Unit Test 是一回事吗?Unit Test是一个伟大的事,尤其是当你去写API和类库的时候。今天XP所说的TDD和UT很不一样。如果你使用TDD来驱动你的软件系统架构,那么,基本上来说,三个迭代以后,你开发的软件就会crash掉,而且无法再往前开发。 因为什么?因为连软件团队自己都受不了这三个迭代出来的架构,而且你还会发现,你根本没去去重构。
  2. 第二个问题是,TDD这种方法破坏了GUI(图形界面),就算是Kent也说:“你永远不可以在一个漂亮的界面后面隐藏一个糟糕的架构”,Coplien强烈地相信软件的架构是通过界面来发出其光芒。他觉得如果没有一个好的软件架构,这个会影响用户的操作。

Coplien接着说,如果我们使用Uncle Bob的三条法则,我们也许没有什么问题,但Coplien想告诉大家另一个非常重要的事,那就是软件架构。并说:“我根本不接受TDD是软件专业化实践的论点”

Bob大叔说,让我们回到99年,那时的敏捷社区觉得软件架构是无关的,不需要软件架构,只需要做一堆tests,做一堆stories,以及足够快的迭代,这样就可以让那些代码魔幻式地拼装起来,这就是horse shit。对于大多数的敏捷拥护者来说,这的确是愚蠢的。今天你再和Knet说这个事,他也会说那不过是一种说法。

Coplien回应到,实际上,Knet在解释XP的时候,在他的书131页的位置说过,“是的,你得做些前期的架构,但也别把自己搞乱了”。

Bob大叔把话题转回来,继续聊关于架构方面的事,他说软件的架构很重要,他也写很一些关于架构的书,他说他也是一个架构方面的怪才,但是他认为架构自己并不会形成软件的所有的外表。他觉得好的软件架构和设计能力应该出现在若干次迭代之后。他觉得你在架构软件的时候,你会创造一些东西,也会破坏一些东西,并且会在几次迭代中做一些试验性的工作,来尝试一下不同的架构。在2到3次迭代以后,你可以知道那一种架构是对的,这样,你可以在后面的迭代中进行调整 。因此,他认为架构是需要进化和发展的,而不会因为被可执行的代码所形成,也不会因为你所写的测试而形成

Coplien赞同架构进化的观点,而且他相信软件的架构的演变和进化不是因为你写的代码,也不是因为Use Case,也不是告诉你你的软件需求的范围和其中的关系,但是如果你做的方法是以增量式的,以用户驱动式的,而你却在和用户沟通时没有一些前期的业务知识,那么这一定是相当有风险的,并且你一定会把事搞砸的。

Coplien接着说,他在Knet早期提到TDD的时候和Knet时,提到YAGNI(陈皓注:You Aren’t Gonna Need It,XP的一个法则,也就是只做最简单的事)时,Kent说到:“让我们来做一个银行帐户,一个储蓄帐户”,储蓄帐户其实就是对余额进行一些加加减减的事,就像一个计算器一样。Copilen继续解释到,但是如果你要做一个真正的银行系统,你的软件架构根本不可能从一个储蓄帐户的对象(计算器)重构出来。因为储蓄帐户根本就不是一个对象,其是一个流程,后面有一个数据库的查帐索引事务,还有存款保证多和利息,还有一些转帐功能。就算是这样,这也只是用户的功能,你还需要支持税务人员和精算会计师等这些人,这会让银行系统成为一个错综复杂的软件架构,这绝对不是你可以用迭代干出来的事。当然,Bob大叔是可以的,因为他有40年的银行系统的经验。但是Bob大叔你的这40年可真不敏捷啊

Coplien接着说, 因为Bob大叔可以在软件前期做很多很重要的决定,这让得后面的事变得相对比较简单。Coplien根本不相信只要你把代码往那一放,在上面披上一层皮,再设置好一些角色,设置好接口,在文档里写上整个业务结构,而你只有在有人花钱的时候你才会在其中填充进真正的代码,反之就违反了你的YAGNI原则。所以,你只是在你需要的时候做你要做的事,但你却还是要提前得到你的软件架构,否则你一定会把你自己逼进死角的。

Bob大叔辩解到,我说的可能和你说的这个有点不同。我们应该不会像你所说的往接口中写一些抽象成员函数,而是创建一些有抽象接口的对象。当然,我不会把一下子为这个对象装载上一堆方法。那些是我需要使用测试驱动或是需求驱动来做的事,我还会随时随地在看是否哪里软件架构可以让我拆分接口。

Coplien说,问题 是你得知道你要干什么?他说他非常同意Knet的书”XP Explained”里说的——“你不能去猜”,然后他举了一个例子,一个他曾经在一个电信项目中重新架构软件的例子,这是一个长途交换机的项目,项目组特别喜欢用面向对象,有一个人需要去做一个“Recovery Object”(应该是系统恢复对象),Coplien说这是很扯的一件事,因为系统恢复根本就不是一个对象,因为他对业务不熟,所以想这么做。而当你在细节上分析的时候,你会发现这根本就不是一个有成员方法的对象。我个人认为,Coplien想用这个例子来说Bob大叔的先定义对象的抽象接口并不是一个好的需求分析的方法。Coplien还说,这个事情今天被资本化成了SOA,真是在玩火啊。

Bob大叔说,这个他很同意。你的确需要知道这个对象的意义是什么。而且他和Coplien都同意应该根据可运行的代码来决定未来,而不是基于投机心理搞一个巨大无比的架构。

此时,Bob大叔把话题又带回原地,他问Coplien:“你需要多少的时间才能写出可运行的代码?是不是一个系统需要写200万行代码才能算?”,Coplien说,在他的经历中,200万行代码算是小项目了,他的项目都是几亿行代码的。而在让代码可以跑起来,他至少需要让所有的对象都联系起来。

Bob追问到,“那么你是怎么测试这些对象的连接性的?”,Coplien说,我当然要测试,我会测试系统启动和停止,看看有没有内存问题,半小时就好了。Bob大叔似乎找到了突破点,于是说到:“Excellent!那么我们间的分歧是什么呢?也许你只是不同意TDD的概念和其专业化,当然,这是另外一个话题了”。

然后,Coplien说了一段我非常非常认同的话——“我看到很多人正在做正确的事,来避免我们之前讨论的那些问题,当然那不是TDD的扩展,而是Dan North所说的BDD。可见,软件开发中很多人在开发软件中都是在用正确的很好的方法,而我对此有意见的是,有人把这个事说成TDD,然后人们就去买相关的书来了解TDD,并且看到“architecture only comes from tests”,我在过去6个月中听到过4次这样的说法,这就像你所说的,完全就是horse shit。而关于你所说的专业化的事,如果你没有见过一个专业化你怎么知道?”。(不是吗?大多数人都知道怎么开发软件,而不是TDD才是专业化的软件开发。)

然后,Bob想多谈谈专业化的事,Bob说,在今天,一个不负责任的程序会提交一段他没有跑过单元测试的代码,所以,要确定你没有把一条没有测试过的代码提交到代码库里的最佳做法就是TDD。

Coplien完全不同意这个说法。他觉得底层的东西是更重要的。他用了一个示例来攻击Bob大叔的这个观点,他先是说代码走查和结对编程都有好的有价值的地方,当然和这个话题不相关。然后他又说了Unit Test,想想我们的单元测试,可能我们的测试案例并不可能测试我们程序中参数的各种状态,这些状态有可能只是半打,有可能是一百个,有可能是2的32次方个,所以,我们可以命中一些状态,也会没有测试到一些状态,我们的测试真的只是试验性的,所以,如果你在测试中发现bug,你真的很幸运。

随后,Coplien推崇了一个叫“Design By Contract” – 契约式设计的方法(我在软件设计中那些方法中提到过,),这个方法认为软件有前验条件,后验条件,还有不变的。这个方法是Eiffel项目使用的一个方法,使用这个方法你可以静态的去做一些检查,相当于你做了一个基础架构来干这些事。Coplien相信这个方法有TDD所有的优点——我需要努力思考我的代码,我需要思考软件的外部接口,而且,Coplien发现这么做会比做测试更有效。这会让你对那些参数的范围考虑地更为宽广,而不是只在测试案例写几个随机分散的值来测试。

今天,Bertrand Meyer(Eiffel语言的创造者,他也不赞同TDD)把这个方法推进了一步,叫CDD – Contract Driven Development,这个是一种关注于对象间关系,其在程序运行前提条件和运行后的后验条中达成一种契约,可以通过对契约条件的动态或静态的检查,来对程序的功能进行验证。这样可以让你更有效地测试程序。这种方法需要对业务的重点部位非常好的了解。这是TDD很难做到的(这就是我在《TDD并不是看上去的那么美》一文中说的TDD的测试范围是个很大的问题)。

Bob大叔似乎在努力回忆CDD和Eiffel,然后他说,TDD不就是干这个的吗?TDD就是把契约变成单元测试,不但测试输入,也测试返回值,这不就是先验条件和后验条件,而且他说,Unit Test和代码结合得更紧,而契约没有和代码结合得紧密,这是他觉得很不舒服的地方。

Coplien说Bob大叔创建了不应该创建的二元论。他说代码在哪里,UT就跟到哪里,代码有多臃肿,UT就有多臃肿,而UT也是代码,也会有BUG,所以,其实这真是事半功倍。还有一个最有名的示例是ADA编译器,其使用了TDD,反而增加了代码中的BUG,因为你的代码多,测试就多,代码就更多,整个代码就太过臃肿。如果你测试中使用了断言,这意味着你就耦合上了代码,你的测试案例和你的代码耦合地越多,你的代码就越难维护。这就是我在《TDD并不是看上去的那么美》一文中说的TDD的代码臃肿和维护问题)

Bob大叔为Coplien对代码臃肿的说法感到惊讶。Coplien说,这就是他的经历,他看到的。Bob大叔承认有很多混乱的测试和混乱的代码,他觉得像XUnit这样的工具被滥用了。Coplien打断道,这不是要和你争论的,我争论的是这就是我看到大家在实践的东西。

Bob大叔反回到,你有没有看到CDD也被滥用的情况?Coplien说,他只觉得目前,软件业对CDD用的还不够。

最后,时间不够了,Bob大叔问了一个不相干的问题,他说,我们这里有BDD,CDD, TDD, 关于DD,他不知道谁是最先第一个使用带DD这个词的,他说他好像记得一个RDD – Responsibility Driven Development。

Coplien对这个问题可能很无语,他只能说——“DD,这是Unix的一个命令嘛,Disk Dump,但这可能算。谢谢你Bob,很高兴又一次见到你 ”

——————————————————正文分割线————————————————————

看完后,我的感觉如下:

  • 这是2008年就在讨论的事,而在2011年我发布了《TDD并不是看上去的那么美》后中国这边才开始讨论。(InfoQ和 Thoughtworks怎么不去找Coplien?)
  • 英语很重要,不懂英语,只看国内的东西,你就容易被洗脑,你就需要更多的时间和精力去思考那些早被人思考过的问题。
  • 开发和测试,都是需要充分地了解业务,充分的思考,充分权衡后才能做得好的事。并不是你用了哪个方法后就专业了,就NB了。
  • 相当BS——上不谈业务,下不谈技术,只谈方法论的人和公司,这是绝对的扭曲。