`

DDD与TDD比较之——TDD

阅读更多

 

DDDTDD比较——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讲述了如何把他们结合起来,读了之后,我获益甚多。

本人著有书籍《漫谈设计模式》

 

分享到:
评论
1 楼 gdpglc 2012-07-27  
好文章。

相关推荐

    BusinessAdminstration:此项目的内容包含DDD,TDD,代码优先,异步任务,KISS,DRY,CLEAN CODE Y SOLID

    下面将详细阐述其中涉及的关键技术点——DDD(领域驱动设计)、TDD(测试驱动开发)、代码优先策略、异步任务处理、KISS原则、DRY原则、CLEAN CODE以及SOLID原则。 1. 领域驱动设计(DDD):DDD是一种软件开发方法...

    ThoughtWorks文集II——敏捷实践的秘密

    这包括遵循最佳实践,如SOLID原则、DDD(领域驱动设计)和TDD。 6. **精益思想**:ThoughtWorks将精益理念应用于软件开发,减少浪费,增加价值流的流动。例如,通过最小化等待时间、消除过度设计和优化工作流程,以...

    实现领域驱动设计.pdf

    最后,书中还会讨论如何将DDD与持续集成、测试驱动开发(TDD)以及敏捷开发方法结合,以实现更高效、更灵活的开发流程。 《实现领域驱动设计》这本书是学习和实践DDD的宝贵资源,它通过实例和深入的解释,帮助...

    领域驱动设计

    ### 领域驱动设计(DDD):精简业务模型与实现匹配 #### 一、领域驱动设计概览 领域驱动设计(Domain-Driven Design,简称DDD)是一种面向复杂业务领域的软件开发方法论,旨在通过深入理解业务逻辑来构建高质量的...

    基于Spring的Java平台程序架构研究.pdf

    为了更好地理解上述设计思想和技术的实际应用,我们可以考虑一个具体的案例——中大型B/S结构的应用系统开发。 1. **领域建模**:在这个案例中,可以将业务划分为多个模块,比如订单管理、库存管理等,每个模块都有...

    重构与设计模式

    TDD(Test-Driven Development)、UDD(Usage-Driven Design)、RDD(Refactoring-Driven Development)、DDD(Domain-Driven Design)等方法论提供了具体的实施策略。 ### 实践与工具 重构、结对编程、静态代码...

    POJOs in Action

    而且在理论与实践之间架起了一座桥梁,使读者能够更好地理解和应用领域驱动设计(Domain-Driven Design, DDD)及测试驱动开发(Test-Driven Development, TDD)等最佳实践。 #### 二、主要内容概述 1. **核心概念...

    魂斗罗java源码-it-glossary:IT术语表:purple_heart:

    DDD - 领域驱动设计 开发人员 - 开发人员 HTML - 超文本标记语言 AI——智能人工(人工智能) GCP - 谷歌云平台 JS - JavaScript LESS - 更精简的样式表 Lib - 图书馆(图书馆) OOP - 面向对象的编程 PHP - 超文本...

    程序员简历模板针对架构师

    1. **全面的IT教育基础**:屈小勇毕业于211工程大学——西南大学的计算机科学与技术专业,具有扎实的理论基础。 2. **深厚的行业经验**:他拥有11年的银行和互联网金融信贷系统实践经验,涵盖了咨询、需求分析、...

    软件工程 课外读物 软件工程思想等

    这通常会涉及到软件工程中的最佳实践,如TDD(测试驱动开发)和DDD(领域驱动设计)。 标签“软件工程”和“案例”表明这些资料将深入探讨实际项目中的问题和解决方案,使学习者能够更好地理解和应对可能出现的各种...

    清洁建筑电影

    通过这个项目,学习者可以了解如何在Dart项目中实施清洁架构,提高代码的可读性和可维护性,并掌握如何在实际项目中运用DDD和测试驱动开发(TDD)方法。同时,这也为其他编程语言的开发者提供了借鉴,让他们了解到...

    领域驱动设计实践

    同时,领域驱动设计并不排斥其他分析技术,如分析模式或测试驱动开发(TDD),它们可以辅助我们构建和验证领域模型。 领域建模在Rational Unified Process(RUP)中也占有重要地位,采用用例驱动的方法,通过用例来...

    timebus1:控制Agregados

    在领域驱动设计(Domain-Driven Design,简称DDD)中,聚合根是业务逻辑的核心组件,它封装了一组相关的实体和值对象,维护其内部一致性。下面我们将深入探讨Java编程语言中如何实现和控制这类聚合体。 首先,让...

    ReCapProjectCarRental

    为了保证代码质量,项目可能还采用了单元测试和持续集成(CI/CD)策略,通过Visual Studio或其他IDE进行开发,利用TDD(测试驱动开发)和DDD(领域驱动设计)方法来确保代码的健壮性和可维护性。 总结来说,...

    cart:领域驱动设计购物车演示

    这是一个域元素,但必须根据项目需求来实现——通过 API 调用或数据库查询。固定价格一旦我们将产品添加到购物车中,价格可能是固定的。 如果是项目用例,请查看。如何组装实际应用我们必须通过我们的基础设施实现...

Global site tag (gtag.js) - Google Analytics