最近我发推文说, 如果我们不知道好的设计是什么样的话 , TDD就无法产生好的设计 。 我还说的是,我们可能应该在TDD之前(或至少同时)教设计。 此推文导致与JB Rainsberger , Ron Jeffries和其他一些人进行了讨论。 后来,我和JB最终在环聊直播中进行了现场讨论 。
如果您回顾我的许多演讲,博客甚至我的书,您会发现我多次说TDD是一种设计工具。 那么,什么改变了? 为什么我不再说相同的话了?
我为什么改变主意?
在更加关注我的工作方式以及其他多少开发人员的工作之后,我意识到没有多少人通过TDD推动良好的设计。 尽管我喜欢“ RED-GREEN-REFACTORING”节奏,但“重构”步骤不足以将TDD称为设计工具。
TDD并未规定您应该如何设计。 它的作用是不断惹恼您,问“您确定吗? 够好吗? 你能做得更好吗?” 这种烦恼(或者不断提醒您看一下您的设计和事情,如果可以改进的话)是一件好事,但还不够。
在我看来,TDD是一种软件开发工作流程 ,可为我带来很多好处,包括不断提醒我使代码更好。 使我的代码更好的含义不是TDD的一部分。
您是否忘了简单设计的4条规则?
啊,是的...但是没有。 我不会忘记他们。 4个简单设计规则不是TDD的一部分,我在这里纯粹是在讨论TDD。 4简单设计规则通常是许多经验丰富的TDD从业人员在重构阶段使用的设计准则(包括我自己,以及其他技术)。
4简单设计规则是我们掌握的众多设计准则之一。 SOLID是另一个。 域驱动设计是另一种。 许多其他设计原则和模式也可以作为良好指导。 这些就是我们在“重构”阶段需要记住的事情。 或者,换句话说,对现有设计准则有充分的了解才能使您获得更好的设计。
TDD是一种工作流(不是设计工具),在重构阶段,您可以将现有的软件设计知识与可以帮助您进行更好设计的设计技术相结合。
并非所有TDD都相同
TDD有两种主要样式,它们之间存在明显差异,主要是在设计方面。
古典主义者
古典主义方法是肯特·贝克(Kent Beck)创建的TDD原始方法。 它也被称为TDD的底特律学校 。
主要特征
- 设计发生在重构阶段。
- 通常,测试是基于状态的测试。
- 在重构阶段,被测单元可能会成长为多个类别。
- 除非使用外部系统隔离,否则很少使用模拟。
- 没有进行前期设计考虑。 设计完全来自代码。
- 这是避免过度设计的好方法。
- 由于基于状态的测试且无需预先设计,因此更易于理解和采用。
- 通常与简单设计的4条规则结合使用。
- 当我们知道输入和期望的输出是什么,但我们真的不知道实现的样子时,对探索很有帮助。
- 非常适合我们不能依赖领域专家或领域语言(数据转换,算法等)的情况
问题
- 公开状态仅用于测试目的。
- 与“从外而内”的方法相比,重构阶段通常更大(以下更多内容)。
- 当在重构阶段出现类时,被测试的单元将大于类。 当我们单独查看测试时,这很好,但是随着类的出现,它们创建了自己的生命,并被应用程序的其他部分重用。 随着这些其他类的发展,它们可能会破坏完全不相关的测试,因为这些测试使用其实际实现而不是模拟。
- 缺乏经验的从业人员通常会跳过重构(设计改进)步骤,从而导致一个循环看起来更像是红色-绿色-红色-绿色-…-红色-绿色大规模的重构。
- 由于其探索性质,一些测试中的类是根据“我认为我将需要使用带有此接口的类(公共方法)”创建的,这使得它们在与系统的其余部分连接时不合适。
- 可能会很慢而且很浪费,因为很多时候我们已经知道在被测类中我们不能承担太多责任。 古典主义者的建议是等待重构阶段修复设计,仅依靠具体证据来提取其他类。 尽管这对新手有好处,但对于经验丰富的开发人员而言,这纯属浪费。
由外而内
从外而内的TDD,也称为伦敦学校或嘲笑者 ,是由伦敦最早的一些XP从业者开发并采用的TDD风格。 后来启发了BDD的创建。
主要特征
- 与古典主义者不同,Outside-In TDD规定了开始测试代码的方向:从外部(接收外部请求的第一类)到内部(将包含满足该功能的单个行为的类)实施)。
- 我们通常从验收测试开始,该测试会验证该功能作为一个整体是否有效。 验收测试还可以作为实施指南。
- 通过失败的验收测试,告知功能尚未完成的原因(没有返回数据,没有消息发送到队列,没有数据存储在数据库中,等等),我们开始编写单元测试。 要测试的第一个类是处理外部请求的类(控制器,队列侦听器,事件处理程序,组件的入口点等)。
- 众所周知,我们不会在单个类中构建整个应用程序,因此我们对要测试的类需要哪种类型的协作者做出一些假设。 然后,我们编写测试来验证被测类及其协作者之间的协作。
- 根据被测试类的公共方法被调用时需要做的所有事情来识别协作者。 协作者的名称和方法应来自领域语言(名词和动词)。
- 一旦测试了一个类,我们将选择第一个协作者(它是没有实现的创建),并按照上一类所使用的相同方法来测试其行为。 这就是为什么要从外而内的原因:我们从更接近系统输入(外部)的类开始,并随着更多协作者的出现而向应用程序内部移动。
- 在编写测试时,设计从红色阶段开始。
- 测试是关于协作和行为的,而不是状态。
- 在重构阶段改进设计。
- 始终创建每个协作者及其公共方法来服务现有的客户端类,从而使代码读起来非常好。
- 与经典方法相比,重构阶段要小得多。
- 促进更好的封装,因为没有任何状态仅出于测试目的而公开,
- 更明确地告诉,不要问方法。
- 更符合面向对象编程的原始思想:测试是关于对象向其他对象发送消息而不是检查其状态的。
- 适用于可以从用户故事和接受条件中提取名称和动词的商业应用。
问题
- 新手很难采用,因为需要更高水平的设计技能。
- 开发人员无法从代码中获得反馈以创建协作者。 他们在编写测试时需要可视化协作者。
- 由于创建过早的类型(合作者),可能导致过度设计。
- 不适合用户故事中未指定的探索性工作或行为(数据转换,算法等)。
- 不良的设计技巧可能会导致模拟爆炸。
- 行为测试比状态测试更难编写。
- 编写测试时,需要具备领域驱动设计和其他设计技术的知识,包括4条简单设计规则。
我们应该使用哪种TDD样式?
都。 所有。 它们只是工具,因此,应根据您的需要使用它们。 经验丰富的TDD从业人员可以从一种样式过渡到另一种样式,而不必担心他们使用的是哪种样式。
宏观和微观设计
有两种设计类型:宏观设计和微观设计。 微型设计是我们在测试驾驶代码时所做的工作,主要使用经典方法。 宏设计超出了我们正在实现的功能。 这是关于我们如何在更高层次上对域进行建模,如何拆分应用程序,层,服务等的内容。宏设计可以帮助我们对应用程序进行整体组织,并为团队和开发人员提供了并行工作而又无需踩踏的方式彼此的脚趾。 宏设计是指企业如何看待应用程序,并且通常使用诸如域驱动设计之类的技术。 宏设计还有助于确保整个应用程序的一致性。 TDD不会帮助您进行宏设计。
使用Outside-In TDD时通常会考虑宏设计,但仅靠Outside-In不足以定义应用程序的宏设计。
结论
多年来,我已经看到许多应用程序已经过测试驱动,但仍然很难使用。 好的,我承认它们比大多数传统应用程序要好得多,这些应用程序之前没有必须维护的测试。
无论开发人员是否编写测试,任何开发人员都可能一团糟。 开发人员还可以测试驱动器废品,无论他们使用哪种TDD样式。
TDD 不是设计工具。 这是一个软件开发工作流程 ,可在其生命周期中提示改进代码。 在这些提示(编写测试和重构)中,开发人员需要了解一些设计准则(4条简单设计规则,域驱动设计,SOLID,模式,Demeter法则,告诉,不要问,POLA / S,按合同设计) ,功能嫉妒,内聚,耦合,平衡抽象原理等),以使其代码更好。 仅仅说重构还不足以将TDD称为设计工具。
许多开发人员指责TDD和模拟程序会降低速度。 他们最终放弃了TDD,因为他们努力获得自己想要的结果。 在我看来,没有开发人员真的很努力地理解RED-GREEN-REFACTOR的生命周期。 他们所苦恼的是如何精心设计软件。
TDD的优点是不断询问我们“嘿,您能使您的代码更好吗? 看看这门课变得越来越困难吗? 好的,您成功了。 这是您的绿色酒吧。 现在做得更好。” 除此之外,您是一个人。
当我们了解好的设计是什么样子时,TDD变得容易得多。 实践和理解可用的大量设计指南将使TDD变得更加容易和有用。 它还将减少其学习曲线,并希望增加其采用率。
极端是坏的。 我们正在从BDUF(前期大设计)转变为根本没有设计 。 抛弃我们的设计知识是一个错误。 当然,我们不应该回到黑暗时代并对所有事物进行过度设计,而认为我们应该只专注于微设计也是一个错误。 如果您是一个人工作,做几篇文章或在一个小型应用程序上工作,那么可以,随您便。 但是,如果您是更大的团队的一员,开发的东西远大于kata,那么,如果您更加关注宏设计和代码结构,将会对您的团队有所帮助。
翻译自: https://www.javacodegeeks.com/2015/05/does-tdd-really-lead-to-good-design.html