DDD与TDD比较之——TDD
DDD与TDD比较——TDD
最近承诺要写一篇TDD和DDD区别的文章,在比较之前,我这里会先分别给出一个DDD的开发实例和TDD的开发实例。这篇文章主要讲解TDD。
最近在做一个金融的项目,很多金融项目都比较陈旧,并且使用了大量store procedure去实现一些业务逻辑,比较难以做单元测试。我所做的那个项目只有几个简单的集成测试和回归测试,没有单元测试,虽然在测试环境运行了很长一段时间,但在发布到生产环境时,仍然出现了很多问题。
很多人跟我说,做单元测试,和快速发布背道而驰,他们是静态的眼光看待问题,单元测试是快速发布的基础,这里我举一个切身最常见的例子:
1.一个开发人员把未做单元测试的代码发布到测试环境,他认为代码写的不错,并且找人对代码做了review,花费单元测试的时间太长了,编码才是20分钟,单元测试就要写30分钟,觉得不划算,于是发布给测试环境。
2.测试人员做了一些的测试,花费了20分钟,发现了一些低级的问题,于是把测试结果等等发送给了开发人员,并和开发人员做了交流,花费了10分钟。
3.打回来重做等流程花费了10分钟。
4.开发人员再次修改,并发布到测试环境,仍然未作单元测试,花费了10分钟;测试人员再进行测试,花费了20分钟,幸运的情况下通过了。
这期间花费的时间是:20(最初编码)+20(测试)+10(额外交流)+10(打回来重做的流程)+10(测试)+20(二次测试)=90分钟
如果有单元测试的情况下:20(编码)+30(单元测试)+20(测试)=70分钟
不难看出未有单元测试导致我们多花了20分钟。
这中间极易出现的额外问题:
1.测试人员和开发人员都是要保证代码尽快的提交到生产环境,但是站的角度不同,开发人员非常有信心表示自己的代码未有问题,测试人员对自己的测试也很有信心,交流过程可能产生火药味儿,如果在关键时机,大家非常忙碌的情况下,可能要花费更多的交流时间耐心地查找问题。
2. 以后对这段代码新增功能,如果编写单元测试,至少花费40分钟(因为维护这段代码的可能会切换的其他人,阅读代码等等,都会出问题)。如果不补上单元测试,非常有可能出现前述问题,并形成恶性循环。
项目上我们经常会遇到这样问题:
1.由于未在萌芽阶段解决问题,BUG发不到生产环境,出现问题
2.未完全测试,导致那段代码在特定条件出现问题,这样依旧要在整个软件生命管理周期花费大量的资源处理出现的问题。
最终导致的结果使大家都很忙,我们做了很多不同的无用功,大家却陷入了水深火热之中。项目逐渐变得迟缓,发布新功能的速度越来越慢。
也有人提出异议,他们的观点主要有以下几点:
1.有些几百行代码的软件是没有必要做过多的测试的,这点我也赞成,为了一些很小的,不常用的功能,或者不需要很多人维护的代码,是不需要谢太多测试。
但对于动辄上十万行,甚至上百万行的软件,自然意义不是非常相同。
2.那些认为单元测试会放慢项目进度的看法往往是静态的,总是以为编码最占用时间的。
孰不知在整个软件项目周期里,编码只占用了一部分,他们没有考虑代码测试失败导致的花费资源。也未料到随着代码增长,功能增多,逻辑更加复杂,一处的修改可能影响很多成熟的功能不能使用,
无法保证引入新的功能对已存在的功能进行破坏,这样更加麻烦。
3.有些非常厉害的人写代码,很少有bug存在,他们是不用写单元测试的。
这也是静态的眼光:一方面,我们不能保证这些代码的后续维护和开发也是这些人;另外一方面,即使最厉害的人,也无法保证他们的程序没有BUG。
既然我们了测试的重要性,我们看看TDD又有什么不同之处:
顾名思义,TDD就是测试驱动的开发,这个和DDD不同,在后续我们会详细讲述他们之间的不同和利弊。所以在TDD,我们信奉的一条哲学是:
If it's worth building, it's worth testing. If it's not worth testing, why are you wasting your time working on it?
那么任何发布的代码都是测试驱动进行开发的,以下举个非常简单的切身体验。
需求是:在客户信息资料中,有护照的信息,我们需要判断护照信息是否合法,对于外国人和中国人的信息,我们以不同的方式进行判断:外国人信息,16位,中国人信息9位,以G开头。
那如何使用TDD进行开发呢?这里我将一步步开始做我们的开发。
第一步,我们不是写出一个类Passport,去而代之我们写出第一个测试isValid_Demestic,代码如下:
@Test public void isValid_Demestic(){ String passport="G334"; boolean valid = new Passport().isValid(passport); assertTrue(valid); }
第二步,我们运行测试代码,发现没有Passport这个类和其的isValid(String passport)方法(注意这个方法签名,后续将会讲到和TDD区别),测试失败。
第三步,我们加入Passport类和方法isValid(String passport),代码如下:
public class Passport { public boolean isValid(String passport) { return true; } }
第四步,运行测试,测试通过。
第五步,加入isInvalid_Demestic
@Test public void isInvalid_Demestic(){ String passport="G3345"; boolean valid = new Passport().isValid(passport); assertFalse(valid); }
第六步,测试未通过,我们再次修改isValid(String passport)方法。
public boolean isValid(String passport) { if (passport == null || passport.equals("")) { return false; } if (passport.startsWith("G") && passport.length() == 8) { return true; } return false; }
第七步,测试通过。
第八步,加入新功能,测试代码是isValid_Foreign
@Test public void isValid_Foreign(){ String passport="F123456789012345"; boolean valid = new Passport().isValid(passport); assertTrue(valid); }
第九步,测试失败,修改isValid(String passport)方法
public boolean isValid(String passport) { if (passport == null || passport.equals("")) { return false; } if (passport.startsWith("G") && passport.length() == 8) { return true; } else if(passport.length() == 16){ return true; } return false; }
第十步,加入新功能,测试代码是isInvalid_Foreign
@Test public void isInvalid_Foreign(){ String passport="Fdd3345"; boolean valid = new Passport().isValid(passport); assertFalse(valid); }
第十一步,测试成功,全部测试通过。
至此,这段代码就可以发布了。或许还有其他问题,我们在这里暂且不再讨论,我们可以看到,我们代码的修改,功能的增加,都是靠测试驱动完成的,只要测试失败,我们就修改代码,否则我们不修改代。如果通过阅读发现现有代码有问题,我们不着急修改现有代码,而是先写测试,如果测试失败,我们再修改代码。
注意:
这中间隐含的一个过程是,我们根据需求,来设计单元测试,要求单元测试能够合理和尽可能覆盖全面的需求和代码逻辑。
然而,聪明的程序员应该发现了一个问题,我们在下面进行讨论。
DDD和TDD的异同:
DDD是Domain驱动的,TDD是测试驱动的,该怎么理解呢?
DDD和TDD都需要首先消化需求,TDD根据需求设计Test Case,使得尽可能的覆盖代码逻辑和需求;而DDD,是模型驱动的,消化完需求后,马上要画出模型,在以后的迭代中,完善更加完整可行的模型,测试变成了其中的一个环节,并非关键环节。
上述示例中,或许有人早已发现了,TDD虽然可以发布那段代码,但是,模型终究是有问题的,虽然TDD在后续的迭代中有机会修复那个模型,但是如果使用DDD,就不会出现这样的失误的:DDD总是最先画出合理的模型。上述Passport修改为如下或许更加合理:
public class Passport { private String passport; public Passport(String passport) { if (passport == null || passport.equals("")) {//better replaced with hasText() throw new IllegalArgumentException("Passport is required!"); } this.passport = passport; } public boolean isValid() { … } }
注意isValid()方法,我们是不需要任何参数的。聪明的人会发现,如果使用TDD,有程序员代码写成如下:
new Passport("Wrong Passport").isValid("G1234567")
以上代码永远是执行正确的,但是我们知道,TDD的上述代码破坏了封装性,这样的程序很可能导致非常严重的错误后果,那么TDD开发时,一些问题是很难发现的,并不是我们不了解OO的封装。
综上:它们二者都是都是敏捷的一种实现,都是迭代的开发过程。如何抉择,我的建议是,使用TDD可以是年轻的团队,OO检验较少的团队。如果使用DDD,必须是有成员对OO有非常深入的了解,有能力也愿意和业务人员一起交流完善模型。
参考:http://www.agiledata.org/essays/tdd.html
作者简介:
我是一个Agile和DDD(Domain-Driven Design)的爱好者,关于这两方面的文章书籍非常丰富:
我非常推荐Eric Evans的Domain-Driven Design: Tackling Complexity in the Heart of Software一书,其中探讨了非常全面的关于领域建模艺术的技术,很多关于DDD的文章和书籍都是基于此书展开的。这本书让我对软件设计有了新的认识,很值得细细回味其中某些重要章节。
Eric Evans的关于Folding together DDD into Agile讲述了如何把他们结合起来,读了之后,我获益甚多。
本人著有书籍《漫谈设计模式》