我应该重构标记为“请勿更改”的代码吗?


148

我正在处理一个相当大的代码库,并且给了我几个月时间来重构现有代码。需要进行重构过程,因为很快我们将需要在产品中添加许多新功能,而就目前而言,我们将无法在不破坏其他功能的情况下添加任何功能。简而言之:我们许多人在他们的职业生涯中都看到过的凌乱,庞大,错误的代码。

在重构过程中,我有时会遇到类,方法或代码行,它们的注释如下:

设置了超时时间,以便给模块A一些时间做事。如果未按时计时,它将损坏。

要么

请勿更改。相信我,你会破裂的。

要么

我知道使用setTimeout不是一个好习惯,但是在这种情况下,我不得不使用它

我的问题是:遇到作者的此类警告时,我应该重构代码吗(不,我无法与作者联系)?


157
恭喜你!您现在是代码的作者。

12
您必须先知道要做什么和要做什么,然后才能解决它(您需要了解后者才能了解更改的全部结果。)问题似乎是同步的,并消除了这些问题。应该重构工作#1,否则每次更改都会使情况变得更糟。您应该问的问题是如何做到这一点。在这个问题上有相当多的语言和平台依赖性。例如:stackoverflow.com/questions/3099794/finding-threading-bugs
sdenham

14
跟进@sdenham的评论:如果您还没有执行代码的自动化单元测试,我建议您花时间创建这样的单元测试,而不是在AFAICT上浪费时间进行“重构女巫”,没有明确的目标心里。一旦有了一套可以全面练习代码基础的单元测试,您将有一种客观的方法来知道您的代码是否/何时需要重写它的代码是否仍然可以正常工作。祝你好运。
Bob Jarvis

17
如果作者如此偏执,以至于有人喘不过气来,代码就会被破坏,那么它可能已经被破坏了,未定义的行为恰好可以解决他们目前想要的方式。特别是第一个听起来好像他们已经竞争了比赛条件,所以大多数时候都可以奏效。斯登纳姆说对了。弄清楚它应该做什么,而不要假设它实际上是什么。

7
@Ethan可能不是那么简单。更改代码可能会以不立即可见的方式破坏代码。有时,在您忘记当前的重构可能导致它之后,它会中断很长时间,因此您所拥有的只是一个带有神秘原因的“新”错误。当它与时间相关时,这尤其可能。
Paul Brinkley

Answers:


225

似乎您在“以防万一”的情况下进行重构,而又不确切知道在进行新功能开发时将详细更改代码库的哪些部分。否则,您会知道是否确实需要重构脆弱的模块,或者是否可以保留它们。

直截了当地说:我认为这是注定的重构策略。您正在将时间和金钱投入到公司中,而没人知道它是否真的会带来收益,并且您正在通过将错误引入工作代码来使事情变得更糟。

这是一个更好的策略:花点时间做

  • 向有风险的模块添加自动测试(可能不是单元测试,而是集成测试)。特别是您提到的那些脆弱模块,在其中进行任何更改之前都需要完整的测试套件。

  • 仅重构您需要进行测试的位。尽量减少任何必要的更改。唯一的例外是当您的测试发现错误时-立即修复错误(并重构到安全地进行错误修复的程度)。

  • 向您的同事讲授“童子军原则”(又名“机会重构”),因此,当团队开始添加新功能(或修复错误)时,他们应该改善并重构需要更改的代码库部分,不少,不多。

  • 为团队获取Feather的书“有效地使用遗留代码”。

因此,当您确定需要更改和重构脆弱的模块的时候到了(由于新功能的开发,或者由于您在步骤1中添加的测试揭示了一些错误),那么您和您的团队就准备好了重构模块,或多或少安全地忽略那些警告注释。

作为对一些评论的答复:公平地说,如果一个人定期怀疑现有产品中的某个模块是引起问题的原因,尤其是标记为“请勿触摸”的模块,那么我同意所有您。应该在该过程中对其进行检查,调试和重构(在我提到的测试的支持下,不一定按此顺序进行)。错误是变更的有力依据,通常比新功能更强大。但是,这是个案决定。必须非常仔细地检查是否值得在标记为“请勿触摸”的模块中更改某些东西的麻烦。


70
我很想拒绝投票,但我会避免。我已经看到了在多个项目上的这种心态会导致更糟糕的结果。一旦故障最终发生,它们往往是灾难性的,并且由于其根深蒂固而难以修复。我会定期修复所见到的污泥,似乎最坏的情况是所修复的问题数量最多,或者最多不会造成已知问题。总体而言,这是一个巨大的优势。然后,代码将更好地用于将来的开发。一针一针可节省九针,但被忽略的问题会变得更糟。
亚伦

98
我认为,任何标有注释的代码“超时都将给模块A一些时间来做事情。如果它不像这样定时,它将崩溃” 已经存在一个错误。没碰到bug真是幸运。
17年

10
@ 17of26:不一定是错误。有时,当您使用第三方库时,您会被迫做出各种“优雅”让步,以使其正常工作。
whatsisname

30
@ 17of26:如果涉嫌存在问题的模块存在错误,那么在进行任何形式的重构之前添加测试就显得尤为重要。当这些测试揭示了该错误时,您就需要更改该模块,因此有必要立即对其进行重构。如果测试没有发现任何错误,最好将代码保持原样。
布朗

17
@亚伦:我不明白为什么您认为添加测试或遵循童子军原则会降低产品质量。
布朗

142

是的,您应该在添加其他功能之前重构代码。

此类注释的麻烦在于它们取决于运行代码库的环境的特定情况。编程时,可能确实有必要在特定点编程超时。

但是,有许多事情可能会改变此等式:硬件更改,操作系统更改,另一个系统组件中无关的更改,典型数据量的更改,您都将其命名。不能保证在今天仍然有必要,或者仍然有足够的保证(本来应该保护的东西的完整性可能已经破坏了很长时间-如果没有适当的回归测试,您可能永远不会注意到)。这就是为什么编程固定延迟以允许另一个组件终止的原因几乎总是不正确的,并且只能偶然地起作用。

就您而言,您不了解原始情况,也无法询问原始作者。(大概您也没有适当的回归/集成测试,或者您可以继续进行测试,告诉您是否破坏了某些东西。)

这看起来像是在不谨慎更改任何内容的论点。但您说无论如何都必须进行重大更改,因此已经存在破坏该位置以前实现的微妙平衡的危险。当您唯一要做的是重构时,现在让苹果购物车心烦意乱,并且要确保如果事情中断是导致重构的重构,那比等到您同时进行其他更改并且永远不要做的更好。确定。


46
最佳答案就在这里;太糟糕了,这是一个失败者。上面接受的答案不是很好的建议,尤其是对“虚假”的强调。去年,我花了最大的精力(仅是我的部分,工作量约为10 ^ 6LOC ),这是我所见过的最遗留的,最糟糕的代码,我习惯于修理掉我看到的火车残骸时间,即使它不是我正在处理的组件的一部分。这样做后,我发现并修复了开发团队甚至不知道的错误(尽管我们的最终用户可能存在)。实际上,我被指示去做。结果产品得到改善。
亚伦

10
我不会称其为重构,而是修复错误。
圣保罗Ebermann

7
重构代码库的设计就是重构。如果重新设计揭示了可以修复的错误,那就是错误修复。我的观点是,第一个通常会导致第二个,而第二个往往很难没有第一个。
Kramii

10
@Aaron,您很幸运在此方面得到管理人员/开发人员的支持。在大多数环境中,由不良的设计和代码引起的已知和未知的错误被视为事物的自然顺序。
Rudolf Olah

5
@nocomprende我会说,如果您对系统的理解不够深刻,无法为它编写一些测试,那么您就无须改变其工作方式。
Patrick M

61

我的问题是:遇到作者的此类警告时,我应该重构代码吗?

不,或者至少还没有。您暗示自动化测试的水平很低。您需要测试后才能放心重构。

目前,我们无法在不破坏其他功能的情况下添加任何功能

现在,您需要专注于提高稳定性,而不是重构。您可以在提高稳定性的同时进行重构,但这是实现您真正目标的工具-稳定的代码库。

听起来这已成为旧版代码库,因此您需要对其有所不同。

首先添加表征测试。不用担心任何规格,只需添加一个可以断言当前行为的测试即可。这将有助于防止新作品破坏现有功能。

每次修复错误时,请添加一个测试用例,以证明该错误已修复。这样可以防止直接回归。

当您添加新功能时,请至少添加一些新功能可以正常工作的基本测试。

也许可以获得“有效使用旧版代码”的副本?

我花了几个月的时间来重构现有代码。

首先提高测试范围。从最困难的区域开始。从变化最大的区域开始。然后,一旦发现不良设计,就一一替换。

您几乎永远不会做一个大型重构,而是每周不断进行小型重构。一个大的重构有破坏事物的巨大风险,并且很难进行良好的测试。


10
+1用于添加表征测试。在修改未经测试的旧代码时,这几乎是最小化回归的唯一方法。如果您开始进行更改后测试开始失败,则可以根据给定的规范检查以前的行为是否确实正确,或者新更改的行为是否正确。
Leo

2
对。或者,如果没有可用的规范检查,则购买该软件或对该软件感兴趣的人们是否确实需要先前的行为。
bdsl

也许可以获得“有效使用旧版代码”的副本?他读那本书需要几个星期?什么时候:在工作场所的工作时间,晚上每个人都睡觉?
Billal Begueradj

23

请记住,切斯特顿(GK Chesterton)的篱笆:在您了解修建原因之前,请不要拆除阻碍道路的篱笆。

您可以找到代码的作者和有问题的注释,并咨询他们以获得理解。您可以查看提交消息,电子邮件线程或文档(如果存在)。然后,您将能够重构标记的片段,或者将您的知识写下来,以便让维护该代码的下一个人可以做出更明智的决定。

您的目的是要了解代码的功能,为什么要在那时用警告标记代码,以及如果忽略警告会发生什么。

在此之前,我不会触摸标记为“请勿触摸”的代码。


4
OP表示“不,我无法与作者联系”。
FrustratedWithFormsDesigner

5
我无法与作者联系,也没有任何文档。
kukis

21
@kukis:您无法联系作者,任何领域专家,也找不到与该代码有关的电子邮件/ Wiki /文档/测试案例?好吧,那是一个完整的软件考古研究项目,而不是简单的重构任务。
9000

12
代码很旧并且作者离开了,这并不少见。如果他们最终为竞争对手工作,那么讨论它可能会违反某人的NDA。在某些情况下,作者永远无法访问:您无法向Phil Katz询问PKzip,因为他几年前去世了。
pjc50

2
如果将“查找作者”替换为“了解代码解决了什么问题”,那么这是最佳答案。有时,这些代码是对花费数周或数月才能发现,跟踪和处理的错误的最可怕的解决方案,而这些错误通常是普通类型的单元测试无法找到的。(尽管有时它们是程序员不知道自己在做什么的结果。您的首要任务是了解它在做什么)
grahamparks

6

带有注释(如您所显示的注释)的代码将在以下情况下成为我重构的事情的重中之重前提是我有任何理由这样做。关键是,代码的臭味非常严重,您甚至可以通过注释闻到它的味道。试图将任何新功能添加到此代码中是毫无意义的,此类代码需要在需要更改时立即消失。

另请注意,这些评论至少没有帮助:它们仅给出警告,没有理由。是的,它们是鲨鱼缸周围的警告标志。但是,如果您想在附近做任何事情,那就没有什么可以尝试与鲨鱼一起游泳了。恕我直言,您应该首先摆脱那些鲨鱼。

就是说,您绝对需要好的测试用例,然后才能敢于使用此类代码。一旦有了这些测试用例,就一定要了解您所更改的每一点,以确保您确实没有更改行为。保留代码的所有行为特殊性是您的首要任务,直到您可以证明它们没有任何作用。请记住,您在与鲨鱼打交道-您必须非常小心。

因此,在超时的情况下:请将其保留,直到您确切地了解代码正在等待什么为止,然后在继续删除超时之前先确定引入超时的原因。

另外,请确保您的老板了解您所从事的工作是什么,以及为什么需要这样做。如果他们拒绝,请不要这样做。您绝对需要他们的支持。


1
有时,代码最终会变得混乱不堪,因为它必须在实际行为与文档不符的硅生产前样品上正确运行。根据变通办法的性质,如果代码永远不需要在越野车芯片上运行,则替换代码可能是一个好主意,但是在没有新代码所需的工程分析级别的情况下更改代码可能不是一个好主意。理念。
超级猫

@supercat我的观点之一是,重构必须完全保留行为。如果它不能保留行为,那将不仅仅是在我的书中进行重构(并且在处理此类旧代码时非常危险)。
cmaster

5

如果一切正常,使用此类警告来重构代码可能不是一个好主意。

但是如果您必须重构...

首先,编写一些单元测试和集成测试,以测试警告提示您的条件。尝试模仿类似生产条件尽可能。这意味着将生产数据库镜像到测试服务器,在计算机上运行相同的其他服务,等等。。。。然后尝试进行重构(当然,在隔离的分支上)。然后在新代码上运行测试,以查看是否可以获得与原始代码相同的结果。如果可以的话,那么它可能确定实现生产的重构。但是,如果出现问题,请准备好撤消更改。


5

我说继续进行更改。信不信由你,不是每个编码人员都是天才,并且这样的警告意味着它可能是一个改进的地方。如果您发现作者是正确的,则可以(略)记录或解释警告的原因。


4

我将评论扩展为答案,因为我认为特定问题的某些方面或者被忽略了,或者被用来得出错误的结论。

在这一点上,是否重构的问题还为时过早(尽管可能会以特定的“是”回答)。

这里的中心问题是(如某些答案中所述)您引用的注释强烈表明该代码具有竞争条件或其他并发/同步问题,例如此处讨论的。由于几个原因,这些都是特别困难的问题。首先,正如您所发现的,看似无关的更改可能会引发问题(其他错误也可能会产生这种影响,但并发错误几乎总是会发生。)其次,它们很难诊断:错误通常会在一个时间或代码与原因相距太远,您所做的任何诊断原因都可能导致其消失(Heisenbugs)。第三,并发错误很难在测试中找到。部分原因是由于组合爆炸:对于顺序代码来说已经很糟糕了,但是添加并发执行的可能交织将其炸毁,使得顺序问题在比较中变得无关紧要。此外,即使是一个好的测试用例,也只会偶尔触发问题-Nancy Leveson计算出Therac 25中的致命错误之一大约在350次运行中发生了1次,但是如果您不知道该错误是什么,甚至没有一个,您就不知道有多少次重复可以进行有效的测试。另外,在这种规模上,只有自动测试可行,并且测试驱动程序可能会施加细微的时序约束,以致于它永远不会真正触发错误(再次出现Heisenbug)。

在某些环境中,有一些用于并发测试的工具,例如Helgrind,用于使用POSIX pthreads的代码,但是我们在此不了解具体细节。如果有适合您的环境的工具,则应在测试中添加静态分析(或者反过来吗?)。

更为困难的是,编译器(甚至是处理器,在运行时)通常可以自由地重新组织代码,而这种方式有时会使推理其线程安全性的做法非常违反直觉(也许最著名的情况是经过双重检查)锁定惯用语,尽管对某些环境(Java,C ++ ...)进行了修改以改善它。)

这段代码可能有一个导致所有症状的简单问题,但是更有可能是您遇到了系统性问题,可能使您计划增加新功能的计划停顿了。我希望我已经说服了您,您可能会遇到严重的问题,甚至可能威胁到产品的生存,而要做的第一件事就是找出正在发生的事情。如果这确实揭示了并发性问题,我强烈建议您先修复它们,甚至在询问是否应该执行更多常规重构的问题之前,以及在尝试添加更多功能之前。


1
您在哪里在评论中看到“比赛条件”?甚至暗示在哪里?您在这里做了很多技术假设,甚至看不到代码的好处。
罗伯特·哈维

2
@RobertHarvey“超时设置为模块A提供了一些时间来做一些事情。” -几乎是竞争条件的定义,并且超时不是处理它们的严格方法。我不是唯一得出这一推论的人。如果这里没有问题,那很好,但发问者需要知道,因为这些注释是处理不佳同步的危险信号。
sdenham

3

我正在处理一个相当大的代码库,并且给了我几个月时间来重构现有代码。需要重构过程,因为不久我们将需要为我们的产品添加许多新功能[...]

在重构过程中,我有时会遇到类,方法或代码行,它们的注释为[“不要碰这个!”]

是的,您应该特别重构那些部分。这些警告是由以前的作者放置在此处的,它的意思是“不要无所事事地篡改这些东西,它非常错综复杂,并且有很多意外的交互作用”。当您的公司计划进一步开发该软件并添加许多功能时,他们专门责成您清理这些内容。因此,您不会无所适从地对其进行篡改,而是要故意承担清理它的任务。

找出那些棘手的模块在做什么,然后将其分解为较小的问题(原始作者应该做的)。为了从混乱中获得可维护的代码,需要对好的部分进行重构,而对坏的部分进行重写


2

这个问题是关于何时/如何重构和/或清理代码的辩论的另一个变体,带有一些“如何继承代码”。我们在具有不同团队和文化的不同组织中都有不同的经验和工作,因此,除了“做您认为需要做的事情,并且以不会让您被解雇的方式做”之外,没有任何对与错的答案。 。

我不认为有很多组织会因为业务支持应用程序需要代码清理或重构而乐于接受业务流程。

在这种特定情况下,代码注释引起了不应该更改这些代码部分的标记。因此,如果您继续前进,而业务确实倒在一边,那么您不仅没有任何东西可以支持您的行动,而且实际上还有一个工件会影响您的决定。

因此,与往常一样,只有在了解了要更改的各个方面之后,才应该谨慎进行并进行更改,并找到测试方法,并密切注意容量,性能和时间,因为代码中的注释。

但是,即使如此,您的管理层仍需要了解您正在做的事情固有的风险,并同意您正在做的事情具有超过风险的商业价值,并且您已经做了可以减轻风险的措施。

现在,让我们所有人回到我们自己的TODO,如果只有更多的时间,我们知道的事情可以在我们自己的代码创建中得到改善。


1

是的,一点没错。这些清楚的迹象表明,编写此代码的人对代码不满意,并且可能会戳它直到他们碰巧起作用。他们有可能不了解真正的问题,或者更糟的是,他们理解了这些问题,并且懒得解决它们。

但是,这是警告,需要大量的努力才能对其进行修复,并且此类修复将具有与之相关的风险。

理想情况下,您将能够找出问题所在并正确解决。例如:

设置了超时时间,以便给模块A一些时间做事。如果未按时计时,它将损坏。

这强烈表明模块A不能正确指示何时可以使用或何时完成处理。也许写这篇文章的人不想打扰修复模块A或由于某种原因而无法做。这看起来像一场灾难,等待发生,因为它表明计时相关性是靠运气而不是适当的顺序来处理的。如果看到此消息,我将非常想修复它。

请勿更改。相信我,你会破裂的。

这不会告诉你太多。这将取决于代码在做什么。这可能意味着它似乎有明显的优化,出于某种原因,它实际上会破坏代码。例如,循环可能碰巧将变量保留为另一代码所依赖的特定值。否则可能会在另一个线程中测试变量,并且更改变量更新的顺序可能会破坏该其他代码。

我知道使用setTimeout并不是一个好习惯,但是在这种情况下,我不得不使用它。

这看起来很简单。您应该能够看到setTimeout正在执行的操作,也许可以找到一种更好的方法来执行此操作。

就是说,如果这些修复不在您的重构范围内,则表明在此代码内尝试重构可能会大大增加您的工作范围。

至少,仔细查看受影响的代码,看看是否至少可以将注释改进到更清楚地说明问题所在。这样可以避免下一个人面对您面临的相同谜团。


2
条件可能已经改变到根本不再存在旧问题的地步,警告注释也变得像旧地图上的“这里是龙”。潜入并了解情况,或了解情况已成为历史。

2
在现实世界中,某些设备没有提供可靠的就绪指示,而是指定了最长的未就绪时间。可靠而有效的准备就绪指示很不错,但有时有人会卡在没有它们的情况下使用。
超级猫

1
@supercat也许吧,也许不是。我们无法从此处的评论中分辨出来。因此,有必要进行调查,以改善评论的细节。
David Schwartz

1
@DavidSchwartz:评论当然可能会更有帮助,但是有时不清楚程序员应该花多少时间来尝试找出设备无法遵守其规格的所有精确方法,特别是如果尚不清楚是否预期会出现问题时,尤其如此是暂时的或永久的事情。
超级猫

1

注释的作者很可能没有完全理解代码本身。如果他们真的知道自己在做什么,他们就会写出实际有用的评论(或者首先不介绍比赛条件)。对我来说,像“ 信任我,您会破坏一切。 ” 这样的注释表示作者试图更改某些内容,从而导致他们无法完全理解的意外错误。

该代码可能是通过猜测和反复试验而开发的,没有完全了解实际发生的情况。

这意味着:

  1. 更改代码是有风险的。显然,这将需要时间来理解,并且可能不遵循良好的设计原理,并且可能会产生模糊的影响和依赖性。它很可能没有经过充分的测试,并且如果没有人完全了解代码的作用,那么将很难编写测试以确保不引入错误或行为改变。赛车条件(如所暗示的内容)特别繁重-这是单元测试无法挽救您的地方。

  2. 更改代码是有风险的。该代码极有可能包含模糊的错误和竞赛条件。如果在代码中发现了严重的错误,或者高优先级的业务需求更改迫使您在短时间内更改此代码,那么您将陷入严重麻烦。现在,您遇到了1中概述的所有问题,但是时间紧迫!此外,代码中的此类“暗区”有传播和感染其所接触的代码其他部分的趋势。

更麻烦的是:单元测试无法挽救您。通常,建议的解决此类旧代码的方法是先添加单元测试或集成测试,然后隔离并重构。但是赛车条件不能通过自动测试来捕捉。唯一的解决方案是坐下来仔细研究代码,直到理解为止,然后重新编写代码以避免出现赛车情况。

这意味着任务比常规的重构要苛刻得多。您必须将其安排为实际的开发任务。

您可以将受影响的代码封装为常规重构的一部分,因此至少可以隔离危险代码。


-1

我要问的问题是为什么有人首先写“请勿编辑”。

我编写了很多代码,其中一些代码很丑陋,并且在当时的给定约束下花费了大量的时间和精力来工作。

在这种情况下,我敢打赌,这种情况已经发生,写评论的人发现有人对其进行了更改,然后不得不重复整个练习并将其放回原处,以使事情正常进行。此后,出于剪切的挫败感,他们写道...请勿编辑。

换句话说,我不想再解决这个问题,因为我有更好的生活习惯。

通过说“不要编辑”,可以说我们知道我们现在将要知道的一切,因此在将来,我们将永远不会学到任何新知识。

关于不编辑的原因,至少应该有一条评论。就像说“请勿触摸”或“请勿输入”。为什么不碰栅栏?为什么不输入?说“电子围栏,请勿触摸!”会更好吗?或“地雷!请勿输入!”。显而易见,为什么,但您仍然可以进入,但至少要先知道后果,然后再做。

我也敢打赌,系统对此魔术代码没有测试,因此没有人可以确认该代码在进行任何更改后能否正常工作。围绕问题代码进行特性测试始终是第一步。请参阅Michael Feathers的“使用旧版代码”,以获取有关在更改代码之前如何打破依赖关系并测试代码的提示。

我认为,最终,人们不愿对重构进行限制,并让产品以自然和有机的方式发展。


2
在先前的9个答案中,似乎并没有提供任何实质性的要点和解释
gnat
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.