何时从头开始重写代码库

时间:2009-06-30 15:37:46

标签: architecture tdd testing

我想回到Joel Spolsky关于永远不会从头开始重写代码的文章。总结一下他的论点:代码不会生锈,虽然在许多维护版本发布之后可能看起来不太好,但如果它有效,它就可以了。最终用户并不关心代码的漂亮程度。

您可以在此处阅读文章:Things You Should Never Do

我最近接手了一个项目,在查看了他们的代码之后,这非常糟糕。我立刻想到了我之前构建的原型,并明确声明它不应该用于任何生产环境。但当然,人们不听。

代码是作为一个网站构建的,没有任何关注点,没有单元测试和代码重复。没有数据层,没有真正的业务逻辑,除非你在App_Code中计算一堆类。

我已向利益相关方提出建议,虽然我们应该保留现有代码,并执行错误修复发布以及一些小功能发布,但我们应该立即开始使用测试驱动开发重写它并明确关注点分离。我正在考虑使用ASP.NET MVC路线。

我唯一关心的当然是从头开始重写可能需要的时间。这并不是完全复杂的,有成员资格的磨坊网络应用程序的运行。

你们有没有遇到过类似的问题?你采取了什么特别的步骤?

更新

那么......我最终决定做什么?我采用了马特的方法并决定重构许多领域。

  • 因为App_Code变得相当 大而且因此减慢了构建 那时候,我删除了许多课程 并将它们转换为一个类 库。
  • 我创建了一个非常简单的数据访问 层,包含所有ADO 调用,并创建了一个SqlHelper对象 执行这些电话。

  • 我实施了清洁日志 解决方案,更简洁。

虽然我不再参与这个项目[资金,政治,等等],但我认为它让我对一些项目的编写能力有了很大的了解,并且开发人员可以采取措施使事情变得更加清洁,随着时间的推移,通过小的增量步骤,可读性更好,更好。

18 个答案:

答案 0 :(得分:59)

仅仅因为现在所有这些问题并不意味着它必须继续拥有它们。如果您发现自己在系统中进行了特定的错误修复,可以从新数据层中受益,那么请创建一个新的数据层。仅仅因为整个网站不使用它并不意味着你不能开始使用它。在修复错误时需要重构。并确保在更改之前准确了解代码的作用。

代码重复问题?下次必须修复重复代码中的错误时,将其拉出到类或实用程序库的中央位置。

而且,正如其他响应者已经提到的那样 - 现在开始编写测试。如果代码听起来像是耦合的话可能很难,但你可能从某个地方开始。

没有充分的理由重写工作代码。但是,如果您已经修复了错误,则没有理由不能使用“更好”的设计来重写代码的特定部分。

答案 1 :(得分:18)

乔尔的文章真的说明了一切。

基本上没有。

正如乔尔指出的那样:你只是从头开始做了太多的失败。这可能比你想象的要长,最终结果是什么?基本上做同样事情的东西。那么商业案例是做什么的呢?

这是一个重要的观点:从头开始写东西要花钱。你将如何收回这笔钱?许多程序员忽略了这一点仅仅是因为他们不喜欢代码 - 有时候是有理由的,有时候不是。

答案 2 :(得分:18)

这本书Facts and Fallacies Of Software Engineering陈述了这个事实: “重用代码的修改特别容易出错。如果要修改超过20%到25%的组件,从头开始重写它会更有效率。” 这些数字来自对该主题进行的一些统计研究。我认为这些数字可能会因代码库的质量而有所不同,因此在您的情况下,考虑到这一说法,从头开始重写它似乎更有效率。

答案 3 :(得分:11)

我有这样的应用程序,重写是非常有益的。但是,您应该尝试避免“改进”陷阱。

当你重写所有内容时,添加新功能并修复一些你没有勇气去触摸的长期问题是非常诱人的。这可能导致功能蠕变,并且还可以极大地延长重写所需的时间。

确保您决定将要更改的内容以及只会重写的内容 - 提前

答案 4 :(得分:7)

我有点不同意那篇文章。在大多数情况下,乔尔是正确的,但有反例表明有时(即使很少)重写是一个好主意。如,

  • Windows NT(远离旧的DOS代码库。在此基础上构建了Win2k,WinXP和即将推出的Win7。是的,Vista也是。旧版本的Windows的最后一个版本是臭名昭着的WinME)
  • Mac OS X(在FreeBSD上重建旗舰产品)
  • 许多竞争对手取代事实上的标准的案例。 (例如,Excel与Lotus 123)

我认为Joel的论点主要基于现有版本中编写得相当好的代码,可以通过后见之明进行改进。无论如何,如果你继承的代码真的那么糟糕,那就推动重写 - 那里有一些可怕的东西。如果它完全可以忍受并且工作得相当好,那么以较慢的速度逐步开始新的东西。

答案 5 :(得分:7)

我是一个小型专职团队的成员,该团队从头开始重写代码,包括早期代码的逆向工程业务规则。原始应用程序是用C ++编写的Web服务(常规崩溃和严重的内存泄漏)和ASP.Net 1.0 Web应用程序,替换是基于C#2.0 asmx的Web服务和带有Ajax的ASP.Net 2.0 Web应用程序。这说明团队所做的一些事情并向管理层解释

  1. 我们支持生产中的现有代码库,直到新代码准备就绪。
  2. 管理层同意重写(第一版)不会引入任何新功能,只会实现现有功能。我们最后只添加了1-2个新功能。
  3. 小团队由经验丰富的开发人员组成,他们具有出色的理解能力和合作。
  4. 在组织中获得C ++人才更加困难,而C#被视为未来维护的更好选择。
  5. 我们同意了一个激进的时间框架,但同时对C#2.0,ASP.Net 2.0等工作充满信心和积极性。
  6. 我们有一个团队负责人来保护我们免受高层管理人员的攻击,我们遵循scrum这样的流程。
  7. 该项目非常成功。它非常稳定,表现更好。稍后,添加新功能会更容易。 因此,我相信在适当的资源和环境下,可以成功完成代码重写。

答案 6 :(得分:6)

只有一个准正当理由浮出水面:政治。

我必须从头开始重写代码库,这与政治有关。基本上,管理代码库的前一个程序员太尴尬了,不能将源代码发布给刚刚被雇用的新团队。她觉得每一个对代码的批评都是对她作为一个人的批评,结果,她只是在被迫时向我们其他人发布代码。她是唯一对源存储库具有管理访问权限的人,每当她被要求释放所有源代码时,她都会威胁要退出并掌握她对代码的所有知识并回家。

这个代码库已有超过15年的历史,并且有各种不同风格的不同人群的卷积和扭曲。这些风格中没有一个显然涉及评论或规格,至少在她发给我们的一小部分中。

由于只有部分代码和截止日期,我被迫进行全部重写。因此我被大吼大叫,因为据说我造成了严重的延误,但我只是低下头,完成了而不是争辩。

政治可能是一个巨大的痛苦。

答案 7 :(得分:4)

我一直处于这种情况,但不是完全重写,而是通过重构过程来改变事物。我遇到的问题是我正在使用的代码的巨大复杂性 - 许多页面都是基于if-cases和复杂的正则表达式,这些都是基于大约十年的无计划增长和扩展而分层的。 / p>

我的目标是通过功能对其进行重构,以便为相同的输入提供相同的输出,但在引擎盖下工作得更加干净和顺畅,以便于未来的增长和提高性能。一般的解决方案是干净而快速的,但是代码上的修复工作变得越来越困难和复杂,因为系统解析的文档中的模糊特殊情况开始显示出来并且我的漂亮干净代码将生成仅仅是输出与原始版本有点太不一样(这是网页,因此不同数量的空白可能会以较小的模糊方式导致旧版IE上的各种布局问题)。

我不知道重新编写的代码是否曾被使用过 - 我在公司有机会完全整合之前离开了公司 - 但我对此表示怀疑。为什么在1500个'if'语句和三行正则表达式可以完成同样的工作时使用20行代码?

答案 8 :(得分:4)

完全重写的一个危险是你的工作经常在线。你的成本不会影响到底线。糟糕的代码是赚钱的代码。

但是,如果你一次修复现有的代码,你就会知道赚钱机器是如何运作的。

答案 9 :(得分:4)

我的回答是:尽可能经常重写

我的职业生涯大部分时间都在继续吟诵粪便,我们礼貌地称之为“程序”,由年轻,缺乏经验的程序员编写,他们被管理者视为“摇滚明星”。这些东西通常是不可修复的,你最终会花费10倍的努力来保持它们一瘸一拐,就像你从头开始重写它们一样。

但我也通过定期重写自己的工作而获益匪浅。每次重写都是以不同方式做事的机会,并且可能更好,并且您应该能够重用旧版本的部分部分。

话虽如此,并非所有重写都是个好主意。例如,Windows Vista。

答案 10 :(得分:3)

在某些时候,你必须减少损失。如果您刚刚继承了此代码库,则可能会进行具有意外后果的更改,并且由于缺少测试,它们几乎不可能找到。

至少,立即开始编写测试。

答案 11 :(得分:3)

而不是从头开始完全重写,您希望在引入单元测试时以小步骤开始重构代码库。例如

  1. 将重复的代码移动到一个公共类中,并在整个项目中重复使用测试
  2. 介绍接口以创建单独的可测试模块。然后,您可以在依赖测试的同时重构接口背后的实现,以确保您不会破坏任何内容。

答案 12 :(得分:3)

我宁愿一点一滴地做一些事情,例如,当你在这些领域工作时,用数据模型创建数据库的后端(即用户首先登录,然后是用户管理,等等),并调整现有的前端使用新的后端(界面驱动,所以你也可以添加测试)。这将使现有代码保留可能的无证调整和行为,您不会通过从头开始重新开发来复制,同时添加一些关注点分离。

过了一段时间,您将迁移大约60%的代码库以使用新的后端,而不需要将工作作为正式项目,只需维护,这样您就可以更好地争取开发时间来做另外40%,一旦完成,现有的前端课程的规模和复杂性将大大减少。完全迁移后,如果您有时间实现新视图,则可以重用新的后端模型和控制器组件。

答案 13 :(得分:1)

首先编写技术规范。如果代码那么糟糕,那么我打赌也没有真正的规范。所以写一个全面而详细的规范 - 如果你想从头开始重写,你还需要编写一个规范,所以时间是一个很好的投资。请注意包含有关该功能的所有详细信息。由于您可以调查应用程序的实际行为,因此这应该很容易。您可以随意添加改进建议,但请务必捕获当前行为的所有详细信息。

作为调查的一部分,您可以考虑编写一些系统的自动化测试来调查和记录预期的行为。专注于黑盒/集成测试而不是单元测试(如果丑陋,代码可能不会允许)。

你有这个规范时,你可能会发现该应用实际上比你的第一印象复杂得多,并重新考虑从头开始重写。如果您决定逐步重构,那么规范和测试将对您有所帮助。但是,如果你仍然决定前进并重写,那么你有一个很好的规范可以从现在开始工作,还有一套集成测试,可以在你的工作完成时为你提供服务。

答案 14 :(得分:1)

经济学中也有一个相互矛盾的说法,

  

永远不要考虑沉没成本

根据维基百科(https://en.wikipedia.org/wiki/Sunk_cost)的沉没成本:

  

在经济和商业决策中,沉没成本是已经发生且无法恢复的成本。

当沉没成本与政治压力或个人自我相结合时(经理想要承认他们做出了糟糕的决定或者没有正确监控结果,即使这是不可避免的或者不是直接的控制?),它会导致称为承诺升级https://en.wikipedia.org/wiki/Escalation_of_commitment)的情况,其定义为:

  

一种行为模式,当个人或团体面临来自某些决策,行动和投资的越来越消极的结果时,将继续而不是改变他们的过程 - 这是不合理的,但与之前的决策和行动保持一致制成。

这如何适用于代码?

现在作为软件开发人员有相当长的职业生涯,我发现的一个共同点是,当面对具有挑战性或丑陋的代码库时(即使它是两年前我们自己的代码库),我们的第一直觉是想要抛弃旧的,丑陋的代码并从头开始重写它。如果它是一个熟悉的代码库,那么这通常源于我们现在比我们启动项目时更熟悉项目和业务需求的缺陷这一事实,因此我们(可能是潜意识地)渴望机会通过完美擦除它们来修复我们过去的罪过。如果它是一个不熟悉的代码库,我们往往过度简化原始开发人员面临的挑战,掩盖"小细节"支持"大画面"由于缺乏对代码最初要解决的业务案例的复杂细节的理解而导致的架构级思维,并且经常会超出预算和时间框架。

然后是技术债务的整个概念,就像金融债务一样,CAN和WILL会导致代码库在技术上破产。越来越多的时间和资源投入到故障排除,灭火和过度挑战的改进中,以至于前进的进展变得昂贵,困难和危险。由于缺陷和项目工作的中断,项目需要更长更长的时间来解决生产问题。下班后"事故"开始成为预期的操作而不是一些罕见的昙花一现我们发现自己处于不得不增加越来越多的技术债务以满足最后期限的地位,而不是退后一步,开始做正确的事情以增加我们未来的生产力(和生活质量) - 技术等同于服用信用卡现金垫款,以便在另一张卡上支付最低金额。

尽管如此,它既不意味着我们应该尽可能地重写,也不应该不惜一切代价避免重写工作代码。两种极端都可能是浪费,而后者确实会导致承诺的升级(因为不惜一切代价意味着完全无视成本,即使这些成本完全超过了利益)。需要做的是对重写代码的成本和收益进行客观评估,而不是进行渐进式改进。挑战在于找到具有专业知识和客观性的人来正确地做出决定。对于我们的开发人员来说,我们通常偏向于重写,因为它往往比处理一些糟糕的遗留代码库更有趣和更有吸引力。业务经理倾向于偏向另一个方向,因为重写会带来一些未知的直接利益。结果通常是没有真正的决定,然后默认继续将时间转储到现有代码中,直到某些情况需要方向转换(或者开发人员隐蔽地重写代码,并且通常会对其进行打屁股)。

我已经研究过一些有点可以挽救的代码库,虽然很难看。他们没有遵循既定的做法或标准,没有使用模式,不是很漂亮,但是他们合理地完成了他们的预期功能,并且足够灵活,可以对其进行修改以满足未来的预期需求。申请的预期寿命。虽然不是很迷人,但是当机会出现时,保持这些代码存活并同时进行渐进式改进是完全可以接受的。否则,除了看起来漂亮之外,其他方面几乎没有什我会说大多数关于我应该重写这个的代码吗?问题出现在这个类别下,我发现自己向团队的初级开发人员解释说,虽然重写会很有趣YetAnotherLineOfBusinessApp在{insert whizzbang framework here}中,它既不是必需的,也不是可取的,这里有一些方法可以改进它......

我还研究过无望的代码库。这些应用程序首先几乎没有启动,通常落后于计划和功能减少的状态。它们的编写方式是除了原始开发人员之外没有人能够理解代码最终的作用。我将此称为"只读"码。一旦编写完成,任何尝试的更改都可能导致系统无法解决的未知来源失败,导致大规模单片代码构造的恐慌批量重写,除了教育当前开发人员对变量巧妙命名的变量实际发生的情况之外没有任何意义。 {1}}当执行到达第1,209行时,在obj_85方法中if... else...switchforeach...语句深层嵌套了7个级别。尝试重构此代码会导致失败。您遵循的每条路径都会带来另一个挑战,更多的路径,然后是分支的路径,然后循环回到前一个路径,经过两个星期的单个类的重构后,您会意识到,虽然可能更好的封装,新代码几乎与旧代码一样糟糕和混淆,可能包含更多错误,因为你重构的原始意图完全不清楚,而且,不知道究竟什么样的商业案例导致最初的灾难,你无法确定您是否完全复制了该功能。进步几乎不存在,因为代码库的翻译几乎是不可能的,如此无辜的重命名变量或使用正确的类型会产生指数量的意外副作用。

尝试改进像上面这样的代码库是徒劳的。重构通常会导致80%的重写,最终结果远不及80%的改进。你最终会得到一些非常不一致的东西,新代码有很多妥协,为了与遗留代码的互操作性而必须实现(其中一半是不必要的,因为新代码需要与之交互的遗留代码)后来无论如何都被重构了)。只有两条路可以遵循......通过黑客攻击继续累积技术债务"修复"和修改,同时希望应用程序在其自身重量崩溃之前被弃用(或者您被转移到另一个项目),或者某人做出业务决策并承担完全重写的风险。我讨厌这两个选项,因为它通常意味着等到关键事件失败或者项目落后于计划,然后你花费接下来三个月的晚上和周末试图获得一些可能永远不会存在的呼吸第一名。

那么,你是如何决定的?

  1. 现有代码的工作情况如何?它是否可靠且相对无缺陷?
  2. 我团队中的人员是否能够通过合理的努力了解此代码的作用?如果我引进一位经验丰富的开发人员,他/她是否能够在合理的时间内充分发挥作用?
  3. 做什么应该是简单的缺陷需要地质时间测量来修复;这么多,以至于我们无法做出真正的改进或达到项目期限?
  4. 代码库是如此脆弱,预期的生命周期使应用程序能够适应未来预期的业务需求是非常值得怀疑的吗?
  5. 现有代码是否真的符合原始功能要求?
  6. 您的组织是否愿意接受投资该应用程序,或者是某人(尤其是组织结构图上更高级别的某人)将会自行解决问题?
  7. 您是否可以提供财务或基于风险的理由,并以事实为依据,为重写提供商业案例?
  8. 如果在完全记录重写的时间和成本(包括开发适当的规范,质量保证测试,后期制作稳定和培训)后,开始重写代码仍然有意义(我们的开发人员往往只会从编码时间的角度考虑?)
  9. 你有选择吗?现有代码是否有可能满足要求(因为如果不是这样,重写大片将成为项目的一部分并被视为"增强"而不是重写)?

答案 15 :(得分:1)

我认为这取决于两件事:

1)遗留代码库的底层设计存在缺陷,

2)重写所需的时间。

1)我工作的公司曾经有一个设计糟糕的代码库,这使得重构非常困难,因为我们不能一次重构一个,主要的问题不是单个类和函数,而是整体设计。因此重构方法将非常困难。 (如果整体设计很好,但是,比如说,个别功能是300行,需要分解,那么重构是有道理的。)

2)尽管代码很多并且非常复杂,但运行流程。我们的发动机并没有做那么多。所以重写不是那么久。有时管理者没有意识到可以在很短的时间内重建成千上万行代码的功能。

我们试图向我们的CTO(小公司)解释这一点,但他仍然认为重写会有风险,因此我和我的同事在大约四个周末重写了引擎的基本功能。然后向我们的CTO展示并最终确信。

现在,如果构建基本功能需要我们六个月,我们就不会有太多争论。

答案 16 :(得分:0)

有一句古老的谚语说:

  

没有糟糕的代码。只有代码可以做什么   你想要的和没有的代码。

知道何时重写的关键在于那里。系统目前是否符合您的要求?如果答案是肯定的,那么缓慢但稳定的改进是最好的选择。如果答案是否定的,那么重写就是你想要的。

回到乔尔的文章,他谈到的代码很乱,但软件是可靠的,并提供了预期的价值。相反,如果代码中包含大量错误的代码不可靠,并且不能覆盖所有用例。你有一些本来应该在那里工作的东西,或者只是失踪了。在这种情况下,所有长出来的小毛都不是修复,而是癌症。

答案 17 :(得分:0)

首先,了解这是一个垂直整合决策。是将COBOL应用程序替换为.NET,还是将一个API(或版本)替换为另一个,或者将存储过程分散到使用它的SQL查询中,还是重构以从函数中提取操作,这取决于对哪种操作进行决定。集成到您的系统中。

麦肯锡(McKinsey)的文章“何时和何时不进行垂直整合”解释了许多有用的内容,我将不再重复,因为我并不完全同意他们所说的一切。 https://www.mckinsey.com/business-functions/strategy-and-corporate-finance/our-insights/when-and-when-not-to-vertically-integrate

对于这个问题,我读的最好的答案是:“问问自己是否竞争。”很抱歉我丢失了那篇文章,但这是您的业务决定。您可以稍后更改。您应该权衡诸如编写代码和测试代码的难度之类的事情,尤其是可以轻松扩展流程和添加新流程-这是横向和纵向的增长,请参考HBR 1978年的文章“您应该如何组织制造”。我的架构在那方面无与伦比。

我们有一个ASPX应用程序,可以在我的体系结构和MVC中进行重写,但是由于对该应用程序进行的更改非常罕见(少于每年),而且更改很小,因此可以更好地利用我的时间。更改界面也会给用户带来麻烦,应该是最后的选择。由于为用户创建了手动数据输入工作,我什至避免向网页添加新字段。即时控制是人们首先要争取的东西,但是当缺乏连续控制时,它就无法与之竞争,例如交换控制权。巡航控制。

存储过程不能与电子表格竞争,因为用户可以理解计算并判断其是否可以过滤财务数据,这与我不得不给管理层带来坏消息的存储过程不同。就是说,没有任何集中式或分布式流程可以与集成流程竞争。集中化成本可控性。

我在这里和那里发现的白皮书说,重构通常是为了集中流程而很少进行分散的。我的体系结构定义了如何组织处理,从而消除了完全重构的持续需求。这是因为它被组织为一个制造系统,无论工艺时间长短,都可以轻松增长并轻松替换步骤。没有任何可提取的东西。