【问题标题】:Red, green, refactor - why refactor?红、绿、重构——为什么要重构?
【发布时间】:2011-04-21 22:17:39
【问题描述】:

我正在尝试学习 TDD 和单元测试概念,并且我看到了口头禅:“红、绿、重构”。我很好奇为什么要在测试通过后重构代码?

这对我来说毫无意义,因为如果测试通过了,那你为什么要搞乱代码呢?我还看到了诸如“只编写足够的代码以使测试通过”之类的 TDD 口头禅。

我能想出的唯一原因是,如果让测试通过绿色,你只是草率地编写任何旧代码。您只需拼凑一个解决方案即可通过测试。那么显然代码是一团糟,所以你可以清理一下。

编辑:

我在另一个 stackoverflow 帖子上找到了这个链接,我认为这证实了我想出的唯一原因,即“通过”测试的原始代码非常简单,甚至是硬编码:http://blog.extracheese.org/2009/11/how_i_started_tdd.html

【问题讨论】:

    标签: unit-testing testing refactoring tdd


    【解决方案1】:

    通常代码的第一个工作版本 - 即使不是一团糟 - 仍然可以改进。所以你改进它,让它更干净,更易读,删除重复,找到更好的变量/方法名称等。这就是重构。既然你有测试,你就可以安全地重构,因为测试会显示你是否无意中破坏了某些东西。

    请注意,通常您不是从头开始编写代码,而是修改/扩展现有代码以添加/更改功能。现有代码可能还没有准备好无缝地适应新功能。因此,新功能的第一次实现可能看起来很笨拙或不方便,或者您可能会发现很难进一步扩展。因此,您可以改进设计,以最简单、最简洁的方式整合所有现有功能,同时仍然通过所有测试。

    你的问题是对古老的“如果它有效,不要修复它”。然而,正如 Martin Fowler 在重构中解释的那样,代码可以以多种不同的方式被破坏。即使它通过了所有测试,也可能难以理解,因此难以扩展和维护。此外,如果它看起来很马虎,未来的程序员将更不会注意保持它的整洁,因此它会更快地恶化,并最终退化为完全无法维护的混乱。为了防止这种情况发生,我们进行了重构,以尽可能保持代码干净整洁。如果我们(或我们的前辈)已经让它变得一团糟,那么重构是一项巨大的工作,对管理层和利益相关者没有明显的直接好处;因此很难说服他们在实践中支持大规模的重构。因此,在每次代码更改后,我们都会以很小甚至是微不足道的步骤进行重构。

    【讨论】:

    • @Péter Török,+1 优雅简洁的答案。
    【解决方案2】:

    我已经看到了口头禅:“红色,绿色,重构。”

    这不是“口头禅”,而是例行公事。

    我还看到了诸如“只编写足够的代码以使测试通过”之类的 TDD 口头禅。

    这是一个准则。

    现在你的问题:

    我能想出的唯一原因是,如果让测试通过绿色,你只是草率地编写任何旧代码。您只需拼凑一个解决方案即可通过测试。那么显然代码是一团糟,所以你可以清理一下。

    你快到了。关键在于 TDD 的“设计”部分。您不仅在编码,还在设计您的解决方案。这意味着确切的 API 可能还不是一成不变的,您的测试可能不会反映最终设计(因为它还没有完成)。在编码“仅足以通过测试”时,您会遇到一些可能会改变您的想法并指导设计的问题。只有在您拥有一些工作代码后,您才能对其进行改进。

    此外,重构步骤涉及整个代码,而不仅仅是您为通过最后一个测试而编写的代码。随着编码的进步,代码的所有部分之间的交互越来越复杂,重构它的最佳时间是在它工作时。

    正是由于这个非常早的重构步骤,您不必担心第一次迭代的质量。这只是一个有助于设计的概念证明。

    【讨论】:

    • 你是一个好斗的人。我喜欢你的风格。
    【解决方案3】:

    很难看出 OP 的怀疑是没有道理的。 TDD 的工作流程植根于避免过早的设计决策,通过强加大量成本(如果不排除的话)“裤子座位”编码,这可能会迅速演变为不明智的 YAGNI safari。[1]

    这种推迟过早设计的机制是“尽可能少的测试”/“尽可能少的代码”工作流程,旨在避免在通常需要解决之前“修复”感知到的缺陷或要求的诱惑或者甚至遇到过,也就是说,可能(应该?)在未来的某个测试用例中解决这个缺点,该测试用例直接映射到一个验收标准,而验收标准反过来又捕获了一个特定的业务目标。

    此外,TDD 中的测试应该 a) 帮助阐明设计要求,b) 解决设计中的问题 [2],以及 c) 作为项目资产,用于捕获和记录应用于特定故事的工作,因此对正确组合的测试进行自我指导的重构工作不仅排除了测试可能提供的任何洞察力,而且还否认了管理和项目规划人员关于实现特定功能的真实成本的信息。[3]

    因此,我建议一个新的测试用例,其目的是在设计中引入一个额外的需求,是解决当前被测代码的风格变化和“重构”之外的任何感知缺陷的正确方法阶段,无论多么善意,都与这一理念背道而驰,实际上是邀请进行 TDD 应该防止的那种不成熟的 YAGNI 设计之旅。我相信罗伯特马丁版本的 3 条规则与这个解释是一致的。 [4 - 公然诉诸权威]


    [1] 前面引用的http://blog.extracheese.org/2009/11/how_i_started_tdd.html 优雅地展示了将设计决策推迟到最后一刻的价值。 (虽然也许斐波那契数列是一个有点人为的例子)。

    [2] 见https://blog.thecodewhisperer.com/permalink/how-a-smell-in-the-tests-points-to-a-risk-in-the-design

    [3] 在待办事项中添加“技术”或“尖峰”故事(是否有味道)将是确保遵循正式流程并记录和证明开发工作的适当方法......如果可以'不要说服产品负责人添加它,那么你不应该浪费时间。

    [4]http://www.butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd

    【讨论】:

      【解决方案4】:

      因为你永远不应该重构非工作代码。如果你这样做了,那么你将不知道错误是最初在那里还是由于你的重构。如果它们在重构之前都通过了,然后又失败了,那么您就知道您所做的更改破坏了某些东西。

      他们并不是要编写任何草率的旧代码来通过测试。最小和草率之间是有区别的。禅宗花园是最小的,但不是马虎的。

      但是,回想起来,您在这里和那里所做的最小更改可能会更好地组合到他们俩都调用的其他一些过程中。在让两个测试分开工作之后,是时候重构了。重构比尝试和猜测一个能最少覆盖所有测试用例的架构更容易。

      【讨论】:

        【解决方案5】:

        您首先使代码行为正确,然后再将其分解。如果您以相反的方式执行此操作,则在修复它时可能会造成混乱/重复/代码异味。

        将工作代码重构为分解良好的代码通常比尝试预先设计分解良好的代码更容易。

        重构工作代码的原因是为了维护。您想要删除重复的原因是,例如只需要在一个地方修复某些东西,并且还知道当您在某个地方修复某些东西时,您不会错过其他地方类似代码中的相同错误。如果变量、方法、类的含义与您最初的意图有所不同,您想重命名它们。

        总体而言,编写工作代码并非易事,编写良好分解的代码并非易事。如果你想同时做这两件事,你可能都没有充分发挥你的潜力,所以先充分注意一个,然后另一个是有用的。

        【讨论】:

          【解决方案6】:

          迭代、进化重构是一种很好的方法,但首先...

          一些不应该不说的事情......

          要在上述一些高级注释的基础上进行构建,您应该了解 复杂系统理论 中的一些重要概念。需要注意的关键概念涉及系统的环境结构、系统如何发展、其行为方式以及其组件如何交互。

          对初始条件的敏感依赖(混沌理论):

          系统的行为将被放大到其最有影响力的趋势——也就是说,如果您有许多Broken Windows会影响开发人员将如何编写下一个模块或与现有模块进行交互,那么这个开发者更有可能打破另一个窗口。它甚至很想打破一扇窗户,因为它是唯一没有打破的。

          熵:

          的定义有很多很多;我发现成为软件工程的一个原因是:系统中不能用于额外工作的能量量。这就是可重用性至关重要的原因。熵主要是在重复逻辑和可理解性方面发现的。此外,这与 蝴蝶效应(对初始条件的敏感依赖)和破碎窗口密切相关——重复的逻辑越多,用于额外实现的 CopyPaste 越多每个实现超过 1X 来维护它。

          变量放大和衰减(涌现理论和网络理论):

          打破一个糟糕的设计是一个很好的实现,尽管它在最初的几次发生时似乎完全崩溃了。这就是为什么拥有一个可以支持多种适应的架构是明智的。当您的系统趋向熵时,您需要一种模块之间正确交互的方式——这就是 接口 的用武之地。如果您的每个模块都无法交互,除非它们同意一致合同。没有这个,你会看到你的系统立即开始适应糟糕的实施——无论哪个轮子最吱吱作响,都会得到油;其他模块会变得很头疼。因此,糟糕的实现不仅会导致更糟糕的实现,而且还会在系统规模上产生不良行为——导致您的系统总体上适应不同的实现并在最高规模上放大熵。发生这种情况时,您所能做的就是不断修补并希望一个更改不会与这些适应相冲突——导致紧急的、不可预测的错误。

          所有这一切的关键是将您的模块封装到它们自己的离散子系统中,并提供一个定义的架构,让它们可以进行通信——例如一个中介。这会将(解耦)行为的集合带入一个自下而上的系统,然后该系统可以将其复杂性集中到一个专门为其设计的组件中。

          使用这种架构方法,您应该不会对“红、绿、重构”的第三个术语感到非常痛苦。问题是,您的 Scrum Master 如何根据对用户和利益相关者的利益来衡量这一点?

          【讨论】:

            【解决方案7】:

            您不应该采取“只编写足够的代码以使测试通过”。口头禅太字面了。 请记住,您的应用程序还没有准备好,因为您的所有测试都通过了。您显然希望在测试通过后重构您的代码,以确保代码可读且架构良好。测试可以帮助您重构,因此重构是 TDD 的重要组成部分。

            【讨论】:

              【解决方案8】:

              首先,感谢您了解测试驱动开发。这是一种很棒的技术,可以应用于许多编码情况,可以帮助您开发一些出色的代码,同时也让您对代码可以做什么和不能做什么充满信心。

              如果您查看 Martin Fowler 的书“重构”封面上的副标题,它也回答了您的问题 -“改进现有代码的设计”

              重构是对代码的转换,不应改变程序的行为。

              通过重构,可以让程序现在和6个月后更容易维护,也可以让下一个开发者更容易理解代码。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-10-27
                • 2015-09-14
                • 2014-01-04
                • 2010-10-22
                相关资源
                最近更新 更多