(翻译) TDD 真的能带来好设计么?

TDD · asj · Created at · Last by hxfirefox Replied at · 2551 hits
377

来源: 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由于发展和推广软件匠艺精神而享有盛名,并经常应邀在世界各地进行讲演。他的职业抱负是通过帮助程序员自我改善并更关注自己的手艺,来让整个软件行业更上一层楼。


「软件匠艺社区」旨在传播匠艺精神,通过分享好的「工作方式」和「习惯」以帮助程序员更加快乐高效地编程。
共收到 4 条回复
1
hkliya · #1 ·

好文章,我更喜欢由外而内,并提前做一些设计。

377
asj · #2 ·

#1楼 @hkliya 我喜欢不提前设计的由外而内

351
abbey · #3 ·

如果缺少预先的分析与设计,纯粹地从TDD入手构建新系统,则很容易迷失在测试的汪洋大海里。BDD区别于传统TDD的地方,就在于BDD至少有一根线把这些测试串起来,能更完整地、更有业务意义地表达出系统行为。

79
hxfirefox · #4 ·

好文,在开始TDD一段时间后,确实有必要有这么一篇文章来提醒我们回过头看看走过的TDD的路,思考我们通过TDD想要的是什么以及如何让TDD应用的更好

需要 Sign In 后回复方可回复, 如果你还没有账号你可以 Sign Up 一个帐号。