[译]TDD 真的能带来好设计么?

来源: http://codurance.com/2015/05/12/does-tdd-lead-to-good-design/
作者:SANDRO MANCUSO

最近我发了一则推特,“TDD 无法带来好设计,如果你不知道好设计长什么样子”,并且说我们可能应该先教授设计再教TDD,(或者至少同时教授)。因为这条推特我和J.B. Rainsberger,Ron Jeffries,以及其他一些人进行了一场讨论。最终J.B和我在Hangout on Air进行了一场现场辩论
如果你回顾我的讲演,blog,包括我的书,你都会发现我多次提到TDD是一种设计工具。那么为什么我现在不再这么主张了呢?

我为什么改变了想法

在仔细观察了我是如何工作的,以及很多其他开发人员如何工作后,我意识到没几个人通过TDD驱动获得了好的设计。尽管我深爱“红灯——绿灯——重构”的节奏,但单单一个“重构”的步骤并不足以使TDD被称为一个设计工具。
TDD并没有开出药方来告诉你应该怎么设计。它只是不停地缠着你,追问你:“你确定写成这样么?是否已经足够好了?你能让它更加完善么?” 这种纠缠或者说是让你保持专注设计和代码改进的提醒,非常的棒,但是还不够。
在我看来,TDD是一种软件开发的流程,它带来了很多的益处,其中包括持续不断的提醒我们改进代码。然而,改进代码到底要改成什么样,TDD却并未涉及。

难道你忘了简单设计四规则么

嗯,是,也不是。我没有忘记。但是简单设计四规则不是TDD的一部分,而现在我在讨论TDD本身。很多经验丰富的TDD践行者通常都把简单设计四规则作为重构阶段设计的指导思想,包括我自己也使用它,以及其它技巧。
简单设计四规则是众多可选的设计思想之一。SOLID是另一选项,领域驱动设计是又一个,另有许多其他的设计原则和模式可以作为很好的设计指导。这些思想正是我们需要装在脑子里来进行重构阶段的。从另一个角度来看,对现有设计思想的透彻理解也会带给我们更好的设计。
TDD是一种流程(而非设计工具),在这个流程中的重构阶段,你可以发挥你已经掌握的的软件设计知识和技巧去帮助自己改进设计。

TDD并非只有一种

TDD有两种主要的风格,它们在何时进行设计有着相当显著的区别。

古典派

古典派是由Kent Beck开创的原本风格,也被称为底特律式TDD

主要特点

  • 设计在重构阶段发生。

  • 一般来说测试是基于状态的测试。

  • 在重构阶段,由测试驱动的单元可能会分化为多个类。

  • 极少使用Mock技术,除非是要与现存系统隔离。

  • 在编码前不进行设计方面的思索。设计完全是从代码中浮现出来的。

  • 是避免过度设计的极佳方法。

  • 基于状态的测试以及无前置设计,使得这种风格更容易理解和采用。

  • 常常结合简单设计四规则一起使用。

  • 当我们知道输入和期望的输出,但是不清楚实现可能是什么样的时候,很适合使用这种方式进行探索。

  • 在我们无法借助行业专家和行业语言(比如数据转换,算法等)的情况下很有帮助。

问题

  • 单纯为了测试暴露状态。

  • 相比由外而内风格,重构阶段通常更大。后面会详细介绍由外而内风格。

  • 当新的类在重构阶段浮现出类时,被测的单元就会超出一个类。如果我们单单看那个测试的话,这没什么问题,然而,当新的类浮现出来后,它就有了自己的生命。它会被程序的其他部分复用。当这个类演化时,可能会破坏其它不相关的测试,因为那些测试使用了它们实际的实现而不是mock对象。

  • 经验不足的练习者往往会跳过重构,也就是设计改进的阶段,导致开发进程变为 红灯—绿灯—红灯—绿灯—...—红灯—绿灯—大重构。

  • 由于探索式的特点,测试驱动下的类产生于“我想我们需要一个有这样接口的类”。可能无法和系统的其它部分很好地对接。

  • 有可能会缓慢费事。我们常常一开始已经知道被测的类不应该有这么多的职责,古典派的建议是等待重构阶段,只在确实有必要时才抽出其它类。对于初学者来说这可能是个好建议,对更有经验的程序员纯粹是浪费时间。

由外而内

由外而内TDD,也被称作伦敦派或者mock式,是由一些XP实践先驱接受和发展出的风格。随后它启发产生了BDD。

主要特点

  • 不同于经典派,由外及内TDD为我们如何着手测试驱动代码做出了规定:从外(接到外部输入的第一个类)到内(各个将会实现系统需要的一个单一特性的类)。

  • 一般从一个验收测试开始,验收测试用来检验是否整个功能工作。验收测试也为实现进行了向导。

  • 验收测试的失败会告知我们功能还有某些部分没有实现的信息(数据没有返回,消息没有送入队列,数据没有存人数据库等等),从中我们可以开始进行单元测试。第一个被测的类负责接收外部的请求(比如一个控制器,队列监听器,事件接收器,组件入口等)。

  • 鉴于我们知道我们不会在一个类里实现整个程序,我们会假定被测试类会需要一些合作类。然后我们在测试里验证被测类与其合作类之间的协作。

  • 通过被测类需要调用合作类公开方法来完成的各种事,可以识别出合作类。合作类的名字和方法名应该来自于行业语言中的名词和动词。

  • 当一个类被完全测试后,我们选取一个合作类(此时应该还没有进行实现),并采用与上一个类相同的方式,通过测试驱动它的行为。这就是我们叫它由外而内的原因:我们从靠近系统输入的地方(外部)通过不断识别合作类,逐步向程序内部推进。

  • 设计起始于红灯阶段,也就是开始写测试时。

  • 测试测的是行为和协作,而非状态。

  • 在重构阶段对设计进行完善。

  • 每个合作类以及它的公开方法都是为了给已有的客户类提供服务的,这使得代码可读性很高。

  • 相比经典式方法,由外而内式的重构阶段要小得多。

  • 提高了封装性,因为不需要仅仅为了测试而暴露状态。

  • 更符合“tell,don't ask”设计方法。

  • 更符合面向对象编程的原本理念:测试是关于对象间发送的消息,而非检测对象的状态。

  • 适用于商业应用,从user story和验收条件中可以获得名词和动词。(作为类名和方法名)。

问题

  • 对于初学者来说很难接受,因为需要更高层次的设计能力。

  • 开发者从代码中无法得到反馈来创建合作类。他们需要通过写测试来使合作类显现出来。

  • 不成熟的创建合作类有可能导致过度设计。

  • 不适用于探索式的工作,以及不特定于user story的行为(数据转换,算法等)。

  • 设计能力糟糕的话可能会导致mock对象爆炸式的激增。

  • 基于行为的测试要比基于状态的测试难写。

  • 在写测试的时候需要具备DDD(领域驱动设计)以及其他设计技能,包括简单设计四规则。

我们应该用那种TDD风格

两种都应该用,不同的风格只是工具而已,应该根据实际需要来采用。经验丰富的TDD实践者会从一种风格换到另一种,根本不操心自己在用那种风格。

宏设计和微设计

有两种设计:宏设计和微设计。当我们用测试驱动代码时我们在进行微设计,主要用于经典式TDD。宏设计超出了我们正在实现的功能。它是关于我们如何在更高层面对领域进行建模,如何划分应用,层次,服务等等。宏设计给我们应用的整体结构,并让不同的团队和开发者可以齐头并进而不会相互阻碍。宏设计关系到商业层面如何看待应用,常常适用领域驱动设计之类的技术。宏设计也帮助保持应用的整体一致性。TDD并不能帮助我们在这个层面进行设计。
当采用由外而内方式TDD时,我们常常会考虑到宏设计,但是由外而内方式本身并不足以定义整个应用的宏设计。

结论

多年以来,我看到了很多由测试驱动开发的应用最终仍然很难维护。当然,我得说比起我以前维护的那些根本没有测试的遗留程序,它们要好得多了。
写或不写测试,程序员都可以搞出一团糟来。用经典式或由外而内式,程序员一样都可以驱动出一坨x。
TDD不是设计工具。它是一种软件开发的工作流程,在流程的整个周期里贯彻着对代码改进的提示。当面临这些提示时(写测试和重构),程序员需要了解一些设计方面的指导方针才能改善代码,如:简单设计四规则么领域驱动设计SOLID,模式,Law of Demeter,Tell Don't Ask,POLA/S,Design by Contract,Feature Envy,内聚),耦合),Balanced Abstraction Principle,等等。仅仅说“重构”并不足以把TDD称为一个设计工具。
很多程序员抱怨TDD和mock让它们写代码更慢了。最终他们都放弃了TDD因为太难得到他们想要的结果。在我看来,没有哪个程序员会对理解红灯——绿灯——重构的循环有什么困难。他们苦苦挣扎的事情,是如何设计良好的软件。
TDD最棒的一点就是它会持续不断的问我们,“嘿,你能把代码改得更好一点么?你看到这个类有多难测试了么?好了,现在这段代码工作了,绿灯。现在改进吧。”但是除了这些,你还是得靠自己来写出更好的代码。
如果你理解了好的设计是长什么样子的,TDD做起来会容易很多。练习并理解现有的丰富设计原则会让TDD容易也有用很多。并且会让TDD的学习曲线平缓下来,也更容易被人接受。
偏激是不好的。我们一下从过重的前期设计(BDUF)变成完全没有设计。抛弃我们已有的设计知识是个错误。当然我们不应该回到过度设计每件事的黑暗时代,然而那种认为我们只需要关注微设计的想法也是错的。如果你是一个人搞搞,做些kata练习,或者开发一个小程序,那么随便你喜欢怎么样开发都可以。但是如果你是团队的一部分,开发一个比练习题大很多的软件,那么还是对你的团队行行好,多关心关心宏设计以及代码结构吧。

关于作者

Sandro Mancuso
软件工匠,作者,伦敦软件匠艺社区(LSCC)的发起人。尽管Sandro从1996年才开始职业生涯,他很早就开始编写代码了。他曾经在创业公司,软件工作室,产品公司,跨国咨询公司以及投行工作过。
在他的职业生涯中Sandro有机会接触各种不同的项目,语言,技术以及行业。他对于在各种规模的组织中推广软件工匠思想以及极限编程有着丰富的经验。Sandro由于发展和推广软件匠艺精神而享有盛名,并经常应邀在世界各地进行讲演。他的职业抱负是通过帮助程序员自我改善并更关注自己的手艺,来让整个软件行业更上一层楼。

欢迎加群讨论TDD相关技术

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结