何时(如果有的话)报废生产代码并重新开始?

时间:2020-03-06 14:50:06  来源:igfitidea点击:

我被要求进行代码审查,并报告在我们的一种新产品中添加新功能的可行性,这是我迄今为止尚未亲自进行过的工作。我知道轻易挑剔别人的代码很容易,但是我想说这很糟糕(试图使目标尽可能客观)。我的代码审查中的一些要点:

  • 线程的滥用:通常使用QueueUserWorkItem和线程,并且线程池委托具有非信息性的名称,例如PoolStart和PoolStart2. 线程之间也缺乏适当的同步,尤其是在UI线程以外的线程上访问UI对象。
  • 魔术数字和魔术字符串:在代码中定义了一些ConstEnum,但是大多数代码依赖于文字值。
  • 全局变量:许多变量被声明为全局变量,并且可能会初始化,也可能不会初始化,这取决于遵循的是什么代码路径以及发生的顺序。当代码也在线程之间跳转时,这会非常令人困惑。
  • 编译器警告:主要解决方案文件包含500多个警告,而总数对我来说是未知的。我从Visual Studio得到了一个警告,它无法再显示任何警告。
  • 半成品类:代码已被处理并添加到这里和那里,我认为这使人们忘记了他们以前做过的事情,因此有一些看似半成品的类和空的存根。
  • 在此未发明:该产品复制了其他产品所使用的公共库中已经存在的功能,例如数据访问帮助器,错误记录帮助器和用户界面帮助器。
  • 关注点分离:我认为有人在阅读有关典型的" UI->业务层->数据访问层" 3层体系结构时,这本书倒挂了。在此代码库中,UI层直接访问数据库,这是因为部分实现了业务层,但由于没有充分充实而被忽略,而数据访问层则控制UI层。大多数低级数据库和网络方法都在对主表单的全局引用上进行操作,并直接显示,隐藏和修改表单。在实际使用相当薄的业务层的地方,它也倾向于直接控制UI。大多数此类较低层的代码还使用MessageBox.Show来在发生异常时显示错误消息,并且大多数吞噬原始异常。当然,这使开始编写单元测试以在尝试重构程序之前验证程序的功能变得更加复杂。

我只是在这方面做文章,但我的问题很简单:花时间重构现有代码库,一次专注于一个问题是否更有意义,还是我们会考虑从头开始重写整个内容?

编辑:为了澄清一点,我们确实有该项目的原始要求,这就是为什么可以重新开始的原因。用另一种方式来表达我的问题的方式是:代码是否能够达到维护它的代价将大于转储它并重新开始的代价的程度?

解决方案

关于这一点有两个思路:我们有原始要求吗?我们对原始要求是否准确有信心吗?那测试计划或者单元测试呢?如果我们准备好了这些东西,可能会更容易。

戴上我的顾客帽子,系统是否正常运行?还是不稳定?如果你有不稳定的东西,那你就有理由改变。否则,最好将其一点一点地重构。

真正报废并重新开始吗?

如果当前代码没有执行我们希望执行的操作,并且更改成本高昂。

我敢肯定,现在有人会链接到Joel的有关Netscape的文章,该文章将他们的代码扔掉,以及它是如此糟糕和巨大的错误。我不想详细讨论它,但是如果我们确实链接了该文章,请在此之前考虑以下问题:IE引擎,该引擎允许MS快速发布IE 4、5、5.5和6. 继承,彻底摧毁了Netscape的IE引擎...这是新的。在他们放弃IE 3引擎之后,Trident是一个新引擎,因为它没有为他们未来的开发工作提供合适的基础。 MS做到了Joel所说的绝对不能做的事情,这是因为MS这样做是因为他们拥有一个使他们完全使Netscape黯然失色的浏览器。因此,请……在链接乔尔之前说一会儿,然后静思一下,说"哦,你永远不要这样做,这是一个糟糕的主意"。

我认为基本维护所需的时间要比实际需要的时间长25%至50%。有一段时间,维护遗留代码变得太昂贵了。最终决定取决于许多因素。时间和成本是我认为最重要的因素。

我同意马丁的观点。我们确实需要权衡从头开始编写应用程序和当前应用程序的状态以及有多少人使用,喜欢它们等方面的工作。通常我们可能要完全从头开始,但是成本远远超过收益。我经常遇到一些看起来很丑陋的代码,但是我很快意识到这些"丑陋"区域中的一些确实是错误修复程序,可以使程序正常工作。

我发现有用的经验法则是,如果给定了代码库,如果我必须重写超过25%的代码以使其起作用或者根据新的要求对其进行修改,那么我们也可以重写从头开始。

原因是到目前为止,我们只能修补一段代码。超出某个特定点,则可以更快地完成。

有一个基本的假设,即我们拥有一种机制(例如全面的单元和/或者系统测试),该机制将告诉我们所重写的版本是否在功能上等同于原始版本(在需要的地方)。

如果我们完全了解应用程序的工作方式(并且完全是我的意思,而不仅仅是对应用程序的运作有一般性的了解),并且我们或者多或者少确切地知道如何制作它,则只能给肯定的改写更好的。任何其他情况都是黑暗中的镜头,这取决于太多的事情。如果可能的话,逐步重构可能会更安全。

如果需要比重新编写整个应用程序更多的时间来阅读和理解代码(如果可能的话),我想将其废弃然后重新开始。

我将尝试考虑系统的体系结构,看看是否有可能在不从头开始的情况下进行剪贴和重写定义明确的特定组件。

通常会发生的情况是,我们可以做到这一点(然后将其出售给客户/管理人员),或者我们发现代码是如此可怕和混乱,我们甚至更加确信需要重写并重新编写代码。对于它还有更多令人信服的论点(包括:"如果我们对其进行正确的设计,则我们将不需要废弃整个东西并进行第三次重写)。

缓慢的维护最终会导致体系结构漂移,从而使以后的重写工作更加昂贵。

If it requires more time to read and understand the code (if that is even possible)
  than it would to rewrite the entire application, I say scrap it and start over.

请非常小心:

  • 我们确定我们不只是懒惰,而且不花心地阅读代码
  • 与其他任何人产生的垃圾相比,我们对要编写的出色代码是否自大?
  • 请记住,经过测试的工作代码比想象中的尚待编写的代码具有更多的价值

用我们尊敬的主人和霸主的话来说,乔尔你不应该做的事情,
放弃工作代码并不总是错误的,但是我们必须确定原因。

如果界面整洁,我们可以清晰地描述模块边界,那么可能值得逐个模块或者逐层重构,以使我们可以将现有客户逐步迁移到更稳定的更干净的代码库中。重构每个模块,我们将重写所有内容。

但是,基于codereview,听起来好像没有任何明确的界限。

尽早并经常废弃旧代码。如有疑问,请将其丢弃。困难的部分是说服非技术人员维护成本。

只要得出的价值似乎大于运行和维护的成本,该软件仍会产生正值。有关重写的问题是:"我们将从重写中获得更多的价值吗?"或者替代地,"我们将从重写中获得更多的价值?"我们将节省多少工时?

请记住,重写投资只是一次。改写投资的回报将永远持续。永远。

将价值问题重点放在具体问题上。我们在上面列出了一堆。坚持下去。

  • "通过扔掉我们不使用但仍然要经过的垃圾,我们是否可以通过降低成本来获得更多价值?"
  • "通过丢弃不可靠并破损的垃圾,我们会获得更多价值吗?"
  • "如果我们理解它,我们是否会获得更多的价值-不是通过记录,而是通过我们以团队建设的方式取而代之?"

你做作业吗?我们将不得不面对以下秀场阻止者。
这些将源自执行人员食物链中某人,其响应如下:

  • "它坏了吗?"当我们说"它并没有因此崩溃"时,他们会说"它没有坏-不要修复它"。
  • "我们已经完成了代码分析,我们了解了它,我们不再需要对其进行修复。"

我们对他们的回答是什么?

那只是第一个障碍。这是最坏的情况。这种情况并非总是会发生,但确实以惊人的频率发生。

行政食物链中的某人会有这样的想法:

  • "重写不能创造足够的价值。与其简单地重写,不如让我们扩展它。"理由是,通过创造足够的价值,用户更有可能接受改写。

通常会注定要扩大范围(人为地扩大价值)的项目。

相反,请执行最小的重写,以替换该死的东西。然后扩展以满足实际需求并增加价值。

我从来没有完全抛出过代码。即使从foxpro系统转到csystem。

如果旧系统有效,那为什么要扔掉它呢?

我遇到了一些非常糟糕的系统。在不需要的地方使用线程。可怕的继承和滥用接口。

最好了解旧代码在做什么以及为什么这样做。然后更改它,以免混淆。

当然,如果旧代码不起作用。我的意思是甚至无法编译。然后,我们可能有理由重新开始。但是,这种情况实际上多久发生一次呢?

是的,这完全有可能发生。我看到这样做可以省钱。

这不是技术决定,而是业务决定。代码重写是长期的收获,而"如果还没有完全解决……"是短期的收获。如果我们刚开始创业时就专注于将产品推向市场,那么答案通常是只使用它。如果我们在一家老牌公司中,或者当前系统的错误导致了更多的工作量,因此需要更多的公司资金..那么他们可能会为此而努力。

尽可能地向总经理提出问题,并在可能的情况下使用美元价值。 "我不喜欢处理"没有任何意义。 "在解决此问题之前,需要花费两倍的时间来完成所有工作",这对很多人来说意义非凡。

我认为这里存在许多问题,很大程度上取决于我们所处的位置。

从客户角度来看,该软件运行良好吗? (如果是,请非常小心更改)。我认为除非系统正在运行,否则除非扩展功能集,否则几乎不会有任何重新认识的地方。我们是否打算扩展该软件的功能和客户群?如果是这样,那么我们还有更多改变的理由。

即使写得好也很困难,即使试图理解别人的代码,写得不好的我也几乎想不到。我们所描述的内容听起来很难扩展。

如果可能的话,当我需要重构基线时,通常我会倾向于随时间重写代码的较小部分。通常存在许多较小的问题,例如幻数,注释不佳等,它们会使代码看上去比实际情况更糟。因此,除非基线糟糕透顶,否则请保留代码并在维护代码的同时进行改进。

如果重构需要大量工作,我建议我们安排一个小的重新设计计划/任务清单,该清单为我们提供了要进行工作的清单,以便可以将基线提高到一个更好的状态。从头开始总是有风险的,并且不能保证完成后代码会更好。使用此技术,我们将始终拥有一个可以随着时间而改进的工作系统。

循环复杂度过高的代码(例如在大量模块中超过100个)是一个很好的线索。另外,它有多少个错误/ KLOC?这些错误有多严重?进行错误修复时,引入错误的频率是多少。如果答案很多(我现在不记得规范),则需要重写。

我会考虑该应用程序是否按照预期的方式工作,是否需要进行修改以及我们是否确信该应用程序已在将要使用的所有场景中进行了全面测试。

如果应用程序不需要更改,请不要花费时间。但是,如果它无法按需运行,并且我们需要控制投入的时间和时间来进行更正,则将其报废并重新编写为团队可以支持的标准。没有什么比可怕的代码更糟糕的了,我们必须支持/解密这些代码,但仍然必须忍受。请记住,墨菲定律说,它将在晚上10时使我们必须使事情正常工作,而这永远不会有效。

我看到一个应用程序在投入生产后的2年内进行了重新架构,而其他应用程序则用不同的技术进行了重写(其中一个是C ++,现在是Java)。在我看来,这两种努力都没有取得成功。

对于较差的软件,我更喜欢采用更具进化性的方法。如果我们可以对旧应用程序进行"组件化",从而可以引入新的需求并与旧代码进行交互,那么我们就可以轻松进入新环境,而不必"出售"零价值(从业务角度出发)的重写。

建议的方法针对我们希望与之交互的功能编写单元测试:1)确保代码按预期运行,以及2)为我们希望在旧基础上进行的任何重构提供安全网。

错误的代码是常态。我认为IT部门因偏爱重写/重新架构/等等而受到了不好的说唱。他们付钱并"信任"我们(作为一个行业)以提供可靠的,可扩展的代码。令人遗憾的是,业务压力经常导致使代码无法维护的快捷方式。有时是糟糕的程序员……有时是糟糕的情况。

要回答我们改写的问题……代码维护成本是否会超过重写成本……答案显然是肯定的。我在示例中看不到任何东西,但是,这使我相信这是情况。我认为可以通过测试和重构来解决这些问题。

我自己没有为此使用指标的经验,但是
文章
"实践中的软件可维护性度量模型"讨论
差不多相同的问题在这里要求他们做两个案例研究。
它从以下编辑说明开始:

In the past, when a maintainer
  received new code to maintain, the
  rule-of-thumb was "If you have to
  change more than 40 percent of someone
  else's code, you throw it out and
  start over." The Maintainability Index
  [MI] addressed here gives a much more
  quantifiable method to determine when
  to "throw it out and start over." This
  work was sponsored by the U.S. Air
  Force Information Warfare Center and
  the U.S. Department of Energy [DOE],
  Idaho Field Office, DOE Contract No.
  DE-AC07-94ID13223.)

生产代码始终具有一定的价值。我真正将其全部扔掉并重新开始的唯一情况是,我们是否确定知识产权受到了不可挽回的污染。例如,如果有人从前任雇主那里带来了大量代码,或者很大一部分代码是从GPLd代码库中窃取的。

我认为规则是...

  • 第一个版本总是丢掉

因此,如果我们学习了自己的课程或者他/她的课程,那么现在我们可以继续写作并重新编写它,因为我们可以更好地了解自己的问题领域。

不是说没有/可以保留的部分。经过测试的代码是最有价值的代码,因此,如果除了样式之外没有其他任何实际缺陷,就没有理由全力以赴。

在没有任何冒犯的意图的情况下,决定从头开始重写代码库是新手软件开发人员常见的,严重的管理错误。

有许多缺点需要警惕。

  • 重写阻止了数月/数年的冷开发新功能。很少有公司能够承受这么长时间的停顿。
  • 大多数开发进度表很难确定。此重写也不例外。现在,通过延迟开发来扩大先前的观点。
  • 通过痛苦的经验修复在现有代码库中的错误,将重新引入。 Joel Spolsky在本文中有更多示例。
  • 沦为第二系统效应受害者的危险-概括而言,``在尝试完成所有他们上次不愿做的事情之前,只设计过一次东西的人就将所有他们在制作第一版时就推迟了一些事情,即使其中的大多数也应该在第二版中也推迟了。''
  • 一旦完成了这一昂贵且繁琐的重写,继承下新代码库的下一个团队很可能会以相同的借口进行另一次重写。程序员讨厌学习别人的代码。没有人会编写完美的代码,因为完美是如此主观的。给我找到任何实际应用程序,我可以为我们提供从零开始的重写和指导。

无论我们最终是否从头开始重写,现在都开始重构阶段是一种真正坐下来并理解问题的好方法,这样,即使真正需要重写,也可以使顺畅进行,并为现有代码库提供诚实的外观真正查看是否需要重写。

越早越好。只要我们预感到代码会慢慢变成丑陋的野兽,这很可能会消耗灵魂并让我们头疼,并且我们知道问题出在代码的底层结构中(因此,任何修复都是黑客,例如引入全局变量),那么该重新开始了。

由于某些原因,人们不喜欢丢掉宝贵的代码,但是如果我们觉得自己从头再来更好,那我们可能是对的。相信自己的直觉,并记住这不是浪费时间,它教会了我们另一种不解决问题的方法。我们可以(应该)始终使用版本控制系统,以使宝宝​​永远不会真正迷路。

每当我看到有关重构的讨论时,我都会发布这本书。每个人都应该阅读Michael Feathers撰写的"有效使用旧版代码"。如果没有别的,我发现它是一本很好的书,读起来很有趣,而且很有激励性。

我想知道那些投票赞成报废和重新开始的人是否曾经成功地重构了一个大型项目,或者至少看到了一个条件恶劣的大型项目,他们认为可以使用重构?

如果有什么话,我会反过来说:我看过4个大项目,一团糟,我主张重构而不是重写。一对夫妇几乎只剩下一行原始代码,并且主要接口以重大方式发生了变化,但是这个过程从来没有涉及到整个项目在超过一周的时间内都无法像最初那样正常运行。 (并且主干从未被破坏)。

也许存在一个严重损坏的项目,以至于尝试进行重构将注定要失败,或者也许我重构过的先前项目之一可以通过"干净重写"得到更好的服务,但是我不确定我会知道的。

当代码到达不可维护或者不可扩展的地步时。充满了短期的hacky修复程序。它具有很多耦合。它有很长的方法(超过100行)。它在UI中具有数据库访问权限。它会产生很多随机的,不可能调试的错误。

底线:维护时比改写更昂贵(即花费更长的时间)。

就业务价值而言,我认为仅凭代码的内部状态就可以进行重写的真实案例非常罕见。如果该产品面向客户并且目前已经投入使用并且能够带来收入(即不是被封存或者未发布的产品),则请考虑以下事项:

  • 我们已经有客户在使用它。他们熟悉它,并且可能已经围绕它建立了一些自己的资产。 (与之交互的其他系统;基于它的产品;必须更改的流程;可能需要对人员进行重新培训)。所有这些花费客户钱。
  • 从长远来看,重新编写它可能比进行困难的更改和修复花费更少。但是,除非应用程序不比Hello World复杂,否则我们还无法量化。重新编写意味着重新测试和重新部署,并且可能是客户的升级途径。
  • 谁说重写会更好?我们能诚实地说公司现在正在编写闪亮的代码吗?将原始代码转换为意大利面条的做法是否得到纠正? (即使罪魁祸首是一个单一的开发人员,他的同僚和管理人员也在哪里,通过审查,测试等确保质量?)

就技术原因而言,我建议如果原始文档具有某些技术上的依赖关系而成为问题,可能是时候进行大规模重写了。例如第三方依赖项,现在已不再受支持,等等。

总的来说,我认为最明智的做法是逐个重构(如果真的很糟糕,则非常小),并逐步改进内部体系结构,而不是大幅度地改进。

何时(如果有的话)报废生产代码并重新开始?

从来没有必要这样做,但是逻辑会(无论如何对我而言)指示,一旦通过拐点,我们花费的时间比在增加新功能上花费更多的时间在返工和修复现有代码库中的错误上,那就该浪费时间了旧的东西,并重新开始。

我曾经相信只是从头开始重写,但这是错误的。

http://www.joelonsoftware.com/articles/fog0000000069.html

改变我的主意了。

我建议的是找出一种正确重构代码的方法。保留所有现有功能并随时进行测试。我们都已经看到了可怕的代码库,但是,随着时间的推移,保持应用程序所拥有的知识是很重要的。