我已经连续两年在两个软件产品公司工作。
第一家是一家小型公司,维护着相当小的管理系统,并具有完整的遗留代码库(将近20年)。在没有足够的单元测试覆盖率的情况下,紧密耦合的代码无处不在。但是,管理层通常不希望开发人员重构遗留代码。
第二个是一家相当大的公司,维护着大型领域特定的系统,并具有庞大的整体Java遗留代码库(超过十年)。分层体系结构确实使基础架构与业务逻辑脱钩。但是,在其业务层中,也有一些巨型类,这些类具有超过3000行代码。开发人员仍然不断向这些旧类中注入越来越多的代码。允许开发人员重构自己的关于添加新功能的相当新的代码,但要警告开发人员也不要重构这些巨大的意大利面类。经验丰富的高级开发人员说,由于缺乏回归测试,对这些类进行更改或重构可能会造成灾难性的后果。
但是,我个人已经阅读了有关干净代码和重构的实用书籍。大多数书籍强烈建议开发人员积极重构。但是,为什么现实世界中的公司反对这一点?
所以我想从经验丰富的开发人员那里收集答案。为什么我所在的这两家公司更喜欢不重构超级遗留代码?这不是灾难性的吗?

评论

大多数书籍强烈建议开发人员积极重构。但是,为什么现实世界中的公司反对这一点? -您已经知道答案。该代码未包含在单元测试中,而且如果没有单元测试来覆盖它,它就太脆弱了,因此,与其以正确的方式花费金钱来编写单元测试和重构,这些公司只是说:“不要碰。它。”这不一定是一个坏策略。如果代码有效,并且再也没有被触及,那么它就永远不会中断,不是吗? (手指交叉)。

有什么不好的事吗?

有书,有现实。

一些答案暗示但没有直接说明的一件事是,现有应用程序不只是一个应用程序。它还包含可能已实现的数千个需求的唯一文档,但从未在项目文档中写下来。它是按实际使用情况进行数十万甚至数百万小时的测试的产品。您不能做的任何事情都可以进行大规模的重构,而又不会有破坏事物的风险。有时,在编码中,两个错误可以成对,而如果修复其中一个错误而不是另一个,则会破坏事物。

我还没有看到可以与在实际环境中运行10年进行比较的测试(是否自动化)。在一年之内,您无法构建的任何东西都几乎就像是已经挑战并修复了十年的代码-它看到了您梦dream以求的极端情况。 -最好还是建造新的东西,只是要知道要像老旧的垃圾一样变得坚韧和战斗力需要时间。

#1 楼

这是风险管理的问题:


重构系统总是会产生破坏以前工作的风险。


系统越大,则它的复杂性越高,破坏某事物的风险就越高。


使用意大利面条式代码(或任何其他结构不良的代码),代码的实际结构仍然模糊,并且依赖项可能被隐藏。一个地方的任何变化都可能轻易影响其他地方。这会增加将某物破坏到最高水平的风险。


使用TDD或任何其他保证一整套测试用例的技术,您可以快速验证重构的零件(以及相关零件) )仍能正常工作。当然,这只有在正确封装的情况下才有效。


不幸的是,遗留代码经常缺少测试,或者它们的覆盖范围或深度不足。 />
换句话说,在具有大量旧式意大利面条代码库的情况下,重构会产生破坏以前工作过的某些事物的高风险,而自动化测试无法降低这种风险的影响。在这种情况下,重构的重大风险仅胜过重构的好处。
补充说明:另一种较低风险的替代方法是:不要接触正在运行的系统,而是使用最新的可测试代码来实现新的和替换的功能并明确界限。这种更具进化性的方法并不总是可行的,但它可以提供显着的短期和长期收益。

评论


说得好,我想补充一点,作为软件开发人员,我们经常对软件质量采取全有或全无的态度,这不太可能受到商业人士的赞赏。完善设计,最先进的系统可以赚很多钱,但是a脚的,过时的系统仍然可以赚钱-当我们以名义上过多地修改现有代码库时,这些钱有可能停止流动代码质量

–毛躁
20/09/26 '16:57

完美设计,最先进的系统首先需要收回成本。但是糟糕的过时的系统已经做到了,因此它实际上赚了更多的钱,尤其是在决定下一个奖金的时间范围内。

– Jan Hudec
20-09-26在19:51

请注意,即使是世界上经过最全面测试的代码之一,sqlite,每个发行版中都修复了一些错误。因此,尽管体面的测试套件是能够完全不打断代码地接触代码的要求,但它仍无法完全消除风险。

– Jan Hudec
20-09-26在19:55

@Christophe:也不只是代码是否有效。如果用有效的新代码Y(在某种意义上说是正确的)替换了已经存在了很长时间的有效代码X,但事情却有所不同,则会激怒和/或使用户群困惑。

–jamesqf
20-09-27在4:46

@jamesqf这是非常有先见之明的。我已经完成了替换现有系统的项目(按照附加说明)。在实践中,它们始终必须是逐个bug的替代品。在复制错误而不是仅仅消除错误方面要做的工作要多得多的情况下,通常必须与用户和其他利益相关者进行互动,以确保他们对更改感到满意。

–James_pic
20-09-28在9:19

#2 楼

原因之一是很难衡量凌乱的代码造成的生产力损失,也很难估计正确清理并修复任何回归所需的工作。
另一个原因是许多开发人员错误地调用了任何重写重构。真正的重构非常有条理,几乎是机械的。您进行了很小的更改以改善结构,但没有更改行为,并且使用自动测试验证了行为没有更改。如果没有好的测试,请先添加它们。
当有人说他们不懂某些代码因此需要重写时,这会使有经验的开发人员感到紧张。除非您进行彻底的简单更改,否则更改不理解的代码将是灾难的根源。
因此,如果您想重构某些代码,请使更改非常简单,以至于任何查看请求请求的人都可以立即看到该行为。被保留。我在真正凌乱的遗留代码中的第一个拉取请求通常只是测试。我的第二个请求是纯粹的机械更改,例如将重复的代码提取到函数中,以及更多利用这些更改的测试。然后,在第三个请求请求中,为清晰起见,我可能会开始重写一些(现在小得多的)函数,因为我已经进行了全面的测试。到这个时候,我已经对代码及其所有怪癖有了相当透彻的理解,并且在第四次请求时,我可能会进行更改,从而影响整体情况。
如果有人试图直接跳到第四次请求,请求,我会强烈反对它,而我从未见过有人反对仅添加测试的请求请求。如果他们不允许您进行高风险的更改,请进行低风险的更改以使您朝同一方向发展。

评论


“除非您进行彻底的简单更改,否则更改您不了解的代码会带来灾难。” –这是切斯特顿篱笆的变形。如果您看不到为什么有东西,请不要触摸它。只有了解了其中的原因后,才能将其删除。同样,来自行话档案的故事也很重要。

–Jörg W Mittag
20年9月26日下午5:00

对于我重构的部分代码,我确实已经了解了,因为我多年来一直在维护旧版代码库。

–芮
20/09/26'7:32

要增加测量问题,通常很难测量更改的正确性。如果公司没有花钱来维持使测试变得容易的各种测试和要求,则尤其如此。

–Cort Ammon
20-09-26在17:49

如您所说,通常不清楚遗留代码有哪些负面影响。同时,很明显,重构将占用其他地方缺乏的资源,并且与往常一样,初始估计可能会低估工作量。这可以与这样的陈述结合起来,即重构对管理人员的好处并不明显,而管理层的确是对的!

–彼得-恢复莫妮卡
20/09/27在12:55



关于“许多开发人员错误地调用任何重写重构”:是的,的确如此(因为重构现在变得很酷-重构是“好”的同义词)。通常,真正(不正确)的意思是没有添加新功能的任何改进:重组,更改/改进接口以及删除似乎不再使用的系统部分(即使删除代码,现在也很酷)您还不完全了解)。 (真实故事-我为此帐户浪费了超过一年的工作时间。)

– Peter Mortensen
20-09-28在4:01



#3 楼

这取决于您对“正确做法”的定义。
我目前正在研究旧的意大利面条代码,其中很多已经可以喝了。这是一个至关重要的安全系统。
对此代码的更改必须由独立的第三方进行审核和签名。公司的所有者认为这是公司的“正确做法”,不要因为重新运行此代码而花费大量预算,因为它的工作方式已经存在多年了。
是的,其中一些代码几乎是垃圾,但这是经过充分测试和信任的垃圾。我们不会仅仅因为如今的螺母和螺栓是最佳实践而拆卸和重新组装铆钉桥。

此外,被认为是软件工程中的“最佳实践”会随着天气的变化而变化。再过十年,意大利面条可能会重新流行起来;)

评论


+1带有现实生活中现实争论的好答案:-)关于您的挑衅性最终声明,我怀疑带有全局变量的goto是否会再次出现。但是,毕竟,意大利面条并不是以前设计的最初目标:随着时间的流逝,它出现了。所以谁知道:我们很可能会在10年后发现,今天的一种或另一种现代实践也存在尚未预料到的缺点...

–克里斯托弗(Christophe)
20-09-27在18:25

我们也不会一直在旧桥上增加新的支柱,通常我们会建造新桥来临时替换旧桥,然后使用现代技术重建旧桥并再次移除临时桥。 @Christophe和大多数重构不是要引入新技术,而是要在多次更改破坏了代码的初衷并使其难以阅读和理解时重新设计。因此,是的,保持旧系统运行是没有问题的,但是您需要定期进行修改的次数越多,重构就越有意义。在这种情况下,可能会使审核的成本降低。

–弗兰克·霍普金斯
20/09/28'2:48

@FrankHopkins与建筑工作的类比有其局限性。以埃菲尔铁塔为例:这是最初应该是暂时的遗产。维护会由于腐蚀而定期更换零件。软件没有同等的东西:软件不会被腐蚀。而且它没有重构,因为结构保持不变。但是有一些扩展,例如附加了最新技术的电梯。但这是为了使整体结构尽可能少地改变。当电梯无法维护,升级时,则将其更换。

–克里斯托弗(Christophe)
20/09/28'6:31

@FrankHopkins,所以发生的事情是随着时间的流逝,旧版保持不变。它是扩展程序,并且与扩展程序接触的部分要重新设计。但是如上所述,尽管我们可以看到一些相似之处,但是软件既不是具体的也不是铁,并且如果还有很多的话,还会有很多;-)

–克里斯托弗(Christophe)
20/09/28'6:36

@EriksKlotins有趣的想法,但是是不同的现象。腐蚀发生在没有任何人对其进行任何更改的材料上:桥梁的铁在与环境接触时会生锈,而无需任何铁匠的干预。该软件不会仅仅因为它被执行并且用户将输入扔给他们而腐烂。关于工程和科学中类比的危险

–克里斯托弗(Christophe)
20-09-28在21:18

#4 楼

我曾经很高兴看到有人“重构”了大约两年前我写的一些旧代码。我的代码很复杂,因为它涵盖了通过密集测试发现的大约二十个极端案例。处理的每个角落案例都有大量文档。
新代码很漂亮。它只有尺寸的四分之一,很容易理解。我使用了旧版本的代码作为参考,并尝试了第一个特殊情况。没用我向新开发人员展示了他,他不明白为什么它不起作用。我完全向他展示了使它起作用的代码以及他删除的代码,而忽略了这些评论。然后我尝试了第二个极端情况,但没有成功。我留给他。我现在在另一个项目上,那是他和他的经理之间的事。从来没有检查过他们所做的事情。
现在,原始代码的作者仍然在那里。现在想象一下十年的代码,原来的作者不在了,与他一起工作的那个人不在了,他走后接了一个人,却没人知道代码的作用和原因。我不是在说您无法重构它,但是您需要一个非常,非常非常好的人,否则您将以奇妙的方式破坏事物。

评论


真实的生活故事。隐藏的案件以什么方式表现出来?它是所用语言的人工制品,还是笨拙的“业务”逻辑。即副作用和副作用如何进入系统-该代码曾经是纯功能性代码吗?

–菲利普·奥克利(Philip Oakley)
20-10-28在11:44

我对您的答案投了赞成票,因为它基本上与我要说的相同。我希望您会考虑进行编辑以明确指出正式需求仅捕获事先已知的内容。规范中没有扩展或更改计划行为的错误修正,性能调整和修改。通常,这些扩充工作很匆忙,并且完全没有记载。除非由同一个团队完成,而他们仍在思考问题,否则重做将放弃所有这些来之不易的知识和经验。

– Peter Wone
20-10-29在5:10



可能是新编码员应该从旧代码注释中编写了单元测试或某种回归测试。然后更新新代码,直到它们通过为止。

–劳尔·诺埃阿(Raul Nohea)善良
20-10-29在20:27

另一个编码员怎么没发现他打破了那些极端情况?您没有为他们编写单元测试吗?单元测试>文档! :D

– FuryFart
20年11月2日,9:44

#5 楼


如何定义“正确”?
当人们试图修复未通过修复意大利面条式代码破坏的内容时,我看到了更多的灾难。
有时您会被迫进行更改。
/>有些人试图完全“正确地”重写它。这些尝试通常花费比预期更长的时间,并且有更多的问题。有时它们会完全失效。
我去过那里,做了那件事,我说如果它还没有破裂,请不要修复它。在转移到其他地方更好的工作后,请留给下一个担心的人。

评论


重写不重构。

– Peter Mortensen
20/09/28'4:11

@Peter:重构未经测试的代码是一种重写。

–mattnz
20-10-29在0:57



#6 楼

您需要更改任何代码的理由。该原因可能很简单,例如死代码(无法访问),使其易于读取(例如,循环到LINQ)或复杂(由于多个相互关联的原因而重构多个模块)。
重构重要的大型代码块时,首先要确定的是更改是否令人满意或什至不是必要,您要确定的第一件事就是更改的安全性。该代码对企业越重要,安全标准就越高。现有错误的相关成本已知,并且尚未对组织造成致命影响。您不想用会导致您倒闭的错误代替它。
这个过程需要数千行,对组织而言非常有价值(而不仅仅是初始化一堆ui控件的位置和值) ),那么在不首先考虑风险的情况下,不应该进行琐碎的更改。
话虽如此,如果要处理3500行,我会考虑对如此大的过程进行的第一个更改将其分解为子过程,您可以将其转换为35个顺序过程:proc1,proc2..proc35,我认为这是一个胜利。
简而言之,是否保留它的决定与代码质量没有直接关系,它是权衡风险与报酬的功能。如果您可以消除风险,则奖励是无关紧要的,相反,风险可能很大,以至于再次使奖励不相关。在大多数情况下,风险将是中等的,直接收益将是最小的。这就是为什么更改无法解决的原因。如果要进行更改,请努力使它们无风险。较小的过程,单元测试,添加文档,以便更好地理解并且不透明。

评论


您的第一段+1。公司不存在,无法开发软件。它的存在是为了赚钱。如果重构没有投资回报,或者回报不足以证明投资是合理的,那它就不会发生。我的工作正好在这里-我想补充一些新功能,但是只有我自己去做,除了削减代码外,我还有其他承诺。这就是管理存在的原因,以便确定什么是优先事项,什么不是优先事项。

–格雷厄姆
20/09/27在12:50

#7 楼

除了好的答案之外,我还想根据自己的经验添加一些内容。
由于各种原因,重写有时并不总是可行的,例如,如果您仍需要不断添加新功能。因此,如果您需要采取逐步的方法进行重构,您将不得不接受仍然存在很长一段时间的遗留代码(或者甚至永远取决于代码库的大小和软件的生命周期) 。
因此,在大多数情况下,由于无法立即重构庞大的代码库,因此在执行重构时,您必须权衡在哪里投入时间。
在这种情况下,我会务实并从头开始在预期收益最高的地方进行重构。例如,这可能是代码中有很多错误的部分,或者是经常发生更改的系统部分。
如果更改发生了很多,那么对该部分进行重构可以带来巨大的好处,因为干净的代码比混乱的代码更容易更改代码。
不经常更改且被认为正确(即没有已知错误)的代码应在重构过程中最后加以处理。或者也许永远不会被触及...
我认为大多数公司都想避免不必要的风险。由于重构遗留代码可能会带来风险,因此应出于正确的理由并首先从中受益的代码承担风险。

评论


好的答案,并添加一些看似微不足道的内容-在重构很少使用的不变的代码之前;首先检查它是否真的被使用。

–汉斯·奥尔森(Hans Olsson)
20-11-2在13:21



同意,未使用的代码为无效代码,无论如何应删除它们。

–afh
20年11月2日,15:23

#8 楼

根据我的经验,


重新发明了可以更轻松地完成的工作,例如使用内置功能。我曾经审核过一个大型的Javascript应用程序,该应用程序使用自定义功能实现了许多现有的Javascript核心功能,甚至使用的功能不一致。


重新设计具有偶像实现或应该使用的设计模式基于众所周知的规格。我经常看到DI容器从头开始制作,而您需要一段时间才能意识到它的全部含义。另一个例子-观察者模式,重塑了almoast,就好像它存在于每个乔的潜意识中一样。另一个例子-SSO协议,提出SAML大约有15年了,OAuth2和OpenIDConnect也曾在这里住了一段时间,但没有,这是“乔完成了,他不再在这里工作了,我们害怕碰它”


不遵循SOLID GRASP建议。设计模式?不,这更糟。就像将3000行代码重构为100行的30种方法一样,它们的名称几乎类似于Foo1Foo30,其中每个FooN在最后一行中调用FooN+1


单元测试要覆盖零个或不足基本案例和极端案例,以便您几乎可以对代码做任何想做的事情,只要看看测试是否通过即可。相反,在没有足够的测试的情况下,人们会担心代码在极端情况下会做一些奇怪的事情,而某些人则依赖于这些极端情况。而且人们仍然不知何故害怕在单元测试中重新创建这些用例,以便他们可以重构代码。


质量始终是成本/时间的总和,并不是您应该照顾所有事情。但是,任何关键部分都可以很容易地分离出来,并通过单元测试进行驯化,然后进行重构。重构的一个好规则是重构,直到您满意为止,然后将每个新的bug更改为涵盖此bug案例的新的单元测试。考虑到这一点,我至少进行了几次令人讨厌的重构,其中完全混乱的东西变成了完全受控的东西。

评论


非常感谢,您的建议确实非常实用,我大多数时候都在以这种方式练习:)

–芮
20-10-23在11:09

#9 楼

我同意卡尔·比勒费尔德(Karl Bielefeldt)的回答,只是添加了一点点。
在庞大的意大利面条代码整体中,经常无法进行部分重构。代码的各个部分紧密耦合,以至于一个点的一项更改需要一个或多个更改,以使代码与一系列更改中的其他点对齐,如此之大,以至于您很容易超出现代敏捷流程的合理范围。因此,要更改代码,人们宁愿等待业务以某种方式进行合理的更改。

评论


甚至可以机械重构(如Karl Bielefeldt所述)的极复杂化的代码(条件goto语句取决于相距较远的某些全局变量,而值取决于其他条件goto语句),作为第一步,将线性代码序列放入函数/程序。在第一步中,可能无法为这些功能找到有意义的名称,但可以稍后进行。随后对代码进行更改要容易得多,并且可以将意大利面条部分与结构更清晰的部分隔离开。

– Peter Mortensen
20-09-28在4:33



(我从事过这样的系统(自由职业者)的工作,从一种系统移植到另一种系统,并在此过程中转换为另一种编程语言。)

– Peter Mortensen
20-09-28在4:33



“天皇方法”中介绍了一种“机械”方法。

–菲利普·奥克利(Philip Oakley)
20-10-28在11:46

“不可能”的说法过于强烈。但是,可以肯定的是,可靠地更改此类代码所需的成本/精力与所产生的收益相比是如此之大,因此不值得这样做。

–史蒂夫·基德(Steve Kidd)
20-10-30在5:08