什么时候从头开始重写代码库
我想回到Joel Spolsky关于从不重写代码的文章。 总结一下他的论点:代码不会生锈,在多次维护版本发布之后可能看起来不太漂亮,但是如果可以的话,它就可以工作。 最终用户不关心代码有多漂亮。
你可以在这里阅读文章: 你不应该做的事情
我最近接pipe了一个项目,查看了他们的代码之后,这真是太糟糕了。 我立即想到了以前build立的原型,并明确表示不应该将其用于任何生产环境。 但是,当然,人们不听。
这个代码是作为一个网站build立起来的,没有任何问题的分离,没有unit testing,而且代码复制到处都是。 没有数据层,没有真正的业务逻辑,除非你在App_Code中计数一堆类。
我已经向利益相关者提出了这样的build议:虽然我们应该保留现有的代码,修复版本以及一些小的function版本,但是我们应该立即开始重写testing驱动开发,并且关注清楚。 我正在考虑去ASP.NET MVC路线。
我唯一担心的就是从头开始重写的时间。 这不是完全复杂的,漂亮的磨坊网站应用程序与会员等运行。
你们有没有遇到类似的问题? 你采取的任何特定的步骤?
谢谢一堆!
更新:
所以..我最终决定做什么? 我采取马特的方法,并决定重构许多领域。
- 由于App_Code变得相当大,从而减慢了构build时间,所以我删除了许多类,并将它们转换为类库。
-
我创build了一个非常简单的数据访问层,其中包含所有的ADO调用,并创build一个SqlHelper对象来执行这些调用。
-
我实施了一个清洁日志
解决scheme,这是更简洁。
虽然我不再参与这个项目(资金,政治,等等),但是我认为这让我对一些项目的写法有多么糟糕,以及一个开发人员可以采取的步骤来使事情变得更清洁,更可读,更公正随着时间的推移逐渐变小。
再次感谢所有评论的人。
因为现在所有这些问题并不意味着它必须继续拥有它们。 如果您发现自己在系统中进行了特定的错误修复,例如可以从新的数据层中受益,则可以创build一个新的数据层。 只是因为整个网站不使用它并不意味着你不能开始使用一个。 重构,因为你需要在错误修复期间。 并确保您在更改代码之前确切了解代码的function。
代码重复问题? 把它拉到一个class级或实用程序库中,在下一次你必须修复重复代码中的错误的中心位置。
而且,正如其他响应者已经提到 – 现在开始编写testing。 如果代码是耦合的,听起来可能很难,但是你也许可以从某处开始。
没有很好的理由来重写工作代码。 但是,如果您已经在修复一个bug,那么没有理由不能用“更好”的devise来重新修改代码的特定部分。
“软件工程事实与谬误”一书陈述了这样一个事实:“修改重用代码特别容易出错,如果要修改一个组件的20%到25%,则从零开始重写它是更加高效和有效的。 “ 数字来自对这个主题进行的一些统计研究。 我认为这些数字可能会因为代码库的质量而有所不同,所以在你的情况下,考虑到这个声明,从头开始重写它似乎更有效率和有效。
乔尔的文章真的说了这一切。
基本上从来没有。
正如Joel所指出的那样:你会从头开始失去太多的东西。 它可能会比你想象的更长,最终的结果是什么? 一些基本上做同样的事情。 那么做什么商业案例 ?
这是一个重要的观点:从头开始写点东西是花钱的。 你将如何收回这笔钱? 许多程序员忽略了这一点,仅仅是因为他们不喜欢代码 – 有时候有理由,有时候不会。
我有这样的应用程序,重写是非常有益的。 但是,您应该尝试避开“改进”陷阱。
当你重写所有的东西的时候,添加新的function和解决一些你没有胆量的长期问题是很有吸引力的。 这会导致特性蠕变,并且还会延长重写所需的时间。
确保你决定什么改变,什么只会被改写 – 提前 。
我一直是一个小的专业团队的一部分,从头重写代码,包括早期代码的逆向工程业务规则。 原来的应用程序是用C ++编写的Web服务(定期崩溃和严重的内存泄漏)和一个ASP.Net 1.0 Web应用程序,replace是一个基于C#2.0 asmx的Web服务和一个带有Ajax的ASP.Net 2.0 Web应用程序。 这就是团队所做的一些事情,并向pipe理层解释
- 我们支持生产中的现有代码库,直到新代码准备就绪。
- pipe理层同意重写(第一版)不会引入新function,只是实现现有function。 最后我们只添加了1-2个新function。
- 这个小团队由非常有经验的开发人员组成,他们的理解能力和合作精良
- 在组织中获得C ++人才很难,C#被看作是未来维护的更好select。
- 我们同意一个积极的时间框架,但同时有信心和积极的工作在C#2.0,ASP.Net 2.0等
- 我们有一个团队领导来保护我们免受高层pipe理人员的影响,我们也跟着stream程一样遵循了stream程
该项目非常成功。 这是非常稳定和更好的performance。 稍后,添加新function将变得更加容易。 所以我相信,在正确的资源和环境下,代码重写可以成功完成。
只有一个准合法的理由浮现在脑海里:政治。
我不得不从头开始重写代码库,这跟政治有关。 基本上,pipe理代码库的以前的编码人员太尴尬,不能将源代码发布到刚刚被雇佣的新团队。 她觉得每一次对代码的批评都是对她的批评,结果她只是在被迫时向其他人发布代码。 她是唯一可以访问源代码库的人,每当她被要求释放所有源代码的时候,她都威胁要退出,并把所有的代码知识带回家。
这个代码库已经超过15年了,并且有各种不同风格的不同人的卷积和扭曲。 至less,这些风格都没有涉及评论或规范,至less在她发布给我们的小部分中。
由于只有部分代码可用和截止date,我不得不彻底重写。 我因此被大吼大叫,因为有人说我造成了严重的延误,但是我只是低下头,把它弄完了,而不是争辩。
政治可以是一个巨大的痛苦。
我有点不同意这篇文章。 Joel的大部分内容都是正确的,但有些反例指出有时(即使很less)重写是一个好主意。 例如,
- Windows NT(从旧的DOS代码库中分离出来),在此基础上构build了Win2k,WinXP和即将推出的Win7,是的,Vista也是,旧版Windows的最后一个版本是臭名昭着的WinME)
- Mac OS X(在FreeBSD上重build他们的旗舰产品)
- 很多情况下,竞争对手取代了事实标准。 (例如,Excel与Lotus 123)
我相信Joel的论点主要是基于现有版本中相当写得很好的代码,可以用后见之明加以改进。 通过一切手段,如果你inheritance的代码真的很糟糕,推重写 – 那里有一些可怕的东西。 如果这一切都是可以忍受的,而且工作得相当好,那么就以较慢的速度把新东西分一杯羹。
我一直在这种情况下,而不是完全重写,我努力通过重构过程来改变事情。 我遇到的问题是我正在使用的代码的巨大复杂性 – 许多可怕的,特殊情况驱动的开发都基于if-case和复杂的正则expression式,在大约十年的无计划的增长和扩展中分层次重叠。
我的目标是通过function获得重构function,以便为相同的input提供相同的输出,但是在发动机罩下工作更加干净平稳,以促进未来的发展并提高性能。 一般的解决scheme是干净而快速的,但是代码上的固定工作变得越来越困难和复杂,因为由系统parsing的文档中不明确的特殊情况开始显示出来,而且我的漂亮的代码会产生刚才的输出有点太不同于原来的(这是网页,所以不同数量的空白可能会导致老式IE版本的各种布局问题)以小而模糊的方式。
我不知道重新编写的代码是否被使用过 – 在我有机会完全集成之前,我离开了这家公司 – 但是我怀疑它。 当一千五百个“if”语句和三行正则expression式可以做同样的工作时,为什么要用二十行代码?
完全重写的一个危险是你的工作不断上线。 你是一个成本,没有贡献的底线。 糟糕的代码是赚钱的代码。
但是,如果你一次只修整现有的代码,那么你就是知道这台钱机如何工作的人。
在某个时候,你必须减less你的损失。 如果您刚刚inheritance了这个代码库,那么您可能会做出意想不到的后果,而且由于缺lesstesting,几乎不可能find。
至less,立即开始写testing。
而不是从头开始完全重写,而是希望在引入unit testing的同时,以小步骤开始重构代码库。 例如
- 将重复的代码移动到通用类中,并在整个项目中进行testing以重新使用
- 引入接口来创build单独的可testing模块。 然后,您可以在依赖testing的情况下重构接口后面的实现,以确保不会破坏任何东西。
我宁愿一点一点地做事情,例如,当你在这些领域工作时(例如,用户先login,然后用户pipe理等等),用数据模型创build数据库的后端,并调整现有的前端 – 使用新的后端(接口驱动,所以你也可以添加testing)。 这将保留现有的代码,可能没有logging的调整和行为,你不会从头开始复制,同时增加了一些关注的分离。
过了一段时间,你将有60%的代码迁移到新的后端,而这些工作并不是一个正式的项目,只是维护,所以你将能够更好地争取开发时间去做另外40个%,一旦完成,现有的前端类将会大大减小,复杂度也会大大降低。 完全迁移后,如果您有时间实施新视图,则可以重新使用新的后端模型和控制器组件。
我的答案是: 尽可能地从头开始重写。
我的大部分职业生涯都是inheritance着一堆被我们称为“计划”的粪便,这些计划是由pipe理人员认为是“摇滚明星”的年轻,缺乏经验的程序员写的。 这些东西通常是不可修复的,你最终花费10倍的时间来努力保持它们的跛行,就像你将它们从头开始重写一样。
但是,我也定期重写自己的工作,从中受益匪浅。 每一次重写都有机会以不同的方式做事情,并且可能更好,而且应该至less可以重用旧版本的某些部分。
这就是说,并不是所有的重写都是一个好主意。 例如Windows Vista。
首先写一个技术规格。 如果代码是可怕的,那么我敢打赌,也没有一个真正的规范。 因此,编写一个全面而详细的规范 – 无论如何,如果你想从头开始重写,那么你需要编写一个规范,所以时间是一个很好的投资。 请注意包括有关function的所有细节。 既然您可以调查应用程序的实际行为,这应该很容易。 随意包含改进build议,但一定要logging当前行为的所有细节。
作为调查的一部分,你可以考虑写一些系统的自动化testing来调查和logging预期的行为。 专注于黑盒/集成testing而不是unit testing(如果代码难看,代码可能不允许)。
当你有这个规范时,你会发现应用程序实际上比你的第一印象复杂得多,并且重新考虑从头开始重写。 如果你决定逐渐重构,规范和testing将会帮助你很多。 但是,如果你仍然决定前进并重写,那么你现在就有一个很好的规范,并且有一套集成testing,当你的工作完成后,这些testing将会电视化。
我认为这取决于两件事情:
1)遗留代码库的底层devise有多么的缺陷,
2)重写的时间。
1)我工作的公司曾经有一个可怕的devise的代码库,这使得重构变得非常困难,因为我们不能一次一个地重构,主要的问题不是单个类和函数,而是整体devise。 所以重构的方法将非常困难。 (如果总体devise是好的,但是,单个函数长300行,需要分解,那么重构是有意义的)。
2)尽pipe有很多代码和非常复杂,运行的过程。 我们的引擎并没有那么做。 所以改写不了多久。 有时候pipe理者不会意识到数十万行代码的function可以在很短的时间内重build。
我们试图向我们的CTO(小公司)解释这一点,但他仍然认为重写会有风险,所以我和我的同事在大约四个周末重写了引擎的基本function。 然后向我们的CTO展示并最终确信。
现在,如果构build基本function需要六个月的时间,我们就不会有太多争论。
在经济学中也有矛盾的说法,
从不考虑沉没成本
按维基百科( https://en.wikipedia.org/wiki/Sunk_cost )计算沉没成本:
在经济和商业决策中,沉没成本是已经发生且不能收回的成本。
当沉没成本与政治压力或个人自我相结合时(pipe理者想成为一个承认他们做出了糟糕决定或者没有适当监督结果的人,即使这是不可避免的,或者不能立即控制的)导致一种称为承诺升级 ( https://en.wikipedia.org/wiki/Escalation_of_commitment )的情况,其被定义为:
一个人或一群人在面对来自某些决策,行动和投资的日益消极的结果时,将继续而不是改变他们的进程的行为模式 – 这是非理性的,而是与先前做出的决定和行动相一致。
这是如何适用于代码?
作为一名软件开发人员,现在有一个相当长的职业生涯,我发现的一个共同点是,当面对一个具有挑战性或难看的代码库(即使它是两年前的我们自己的代码库)时,我们的第一本能是想抛出从旧的,丑陋的代码,并从头开始重写。 如果这是一个熟悉的代码库,那么这通常是由于我们现在比我们开始项目时更熟悉项目和业务需求的陷阱,所以我们(或许潜意识地)渴望这个机会用完美的方法去清除我们过去的罪恶。 如果这是一个陌生的代码库,我们往往倾向于过度简化原始开发人员所面临的挑战,掩盖“小细节”,转而采用“全景图”架构级思维,而且经常由于某种原因而浪费预算和时间表缺乏对代码原本要解决的商业案例的复杂细节的理解。
那么就有技术债务的概念,就像金融债务一样,CAN和将会累积到一个代码库在技术上破产的地步。 越来越多的时间和资源投入到排除故障,扑灭火灾,以及过度挑战性的改进上,使得前进的步伐变得昂贵,困难和危险。 由于缺陷,项目需要更长和更长的时间,并被从项目工作中解脱出来以解决生产问题。 在几小时之后,“事件”开始变成预期的操作,而不是一些罕见的事件。 为了提高我们未来的生产力(和生活质量),我们不是退后一步,而是开始做正确的事情,我们发现自己处于一个不得不增加技术性债务的位置,以便按期完成工作 – 用信用卡预付现金,以便在另一张卡上支付最低限度的费用。
无论如何,这既不意味着我们应该尽可能地重写,也不应该不惜一切代价来重写工作守则。 这两个极端都是潜在的浪费,而后者往往会导致承诺的升级(因为不惜一切代价意味着完全无视成本,即使这些成本完全超过了收益 )。 需要进行的是对重写代码的成本和收益进行客观评估,而不是逐步改进。 面临的挑战是find一个既有专业知识又有客观的人来做出正确的决定。 对于我们的开发人员来说,我们通常偏向于重写,因为它往往比在一些糟糕的遗留代码基础上工作更有趣和吸引人。 业务经理倾向于偏向另一个方向,因为重写强加了一些未知的知觉,而且没有什么直接的好处。 结果通常是没有一个真正的决定,然后默认继续将小时转储到现有的代码中,直到某些情况需要定向转移(或者开发人员秘密地重写代码,并且通常会被打分)。
我曾经研究过可以挽救的代码库,虽然很难看。 他们没有遵循既定的惯例或标准,也没有使用模式,也不是很漂亮,但是他们完成的function相当好,而且足够灵活,可以修改它们以满足应用程序预期的未来需求。 虽然不是魅力四射的,但是在机会出现的时候保持这个代码的活力,同时进行渐进式的改进是完全可以接受的。 否则,除了看起来漂亮以外,其他方面都没有什么好处。 我会说,大部分的代码应该重写这个? 问题出现在这个范畴之下,我发现我自己向团队中的初级开发者解释说,虽然在{插入whizzbang框架}中重写YetAnotherLineOfBusinessApp将会很有趣,但这并不是必要的,也不是必须的,下面是一些方法我们可以改善它…
我也研究过无可救药的代码库。 这些应用程序首先几乎没有启动,通常落后于进度和function降低状态。 他们的写作方式是除了最初的开发者之外没有人能够理解代码最终做什么。 我把这称为“只读”代码。 一旦写入,任何尝试的变化都可能导致系统无法解读的未知来源的失败,从而导致大规模整体代码构造的恐慌批注重写,除了教育当前开发者实际发生的变化巧妙地命名为obj_85
在执行到达1,209行的时候,在DoEverythingAndMakeCoffee(...)
方法中的if... else...
, switch
和foreach...
语句的某处嵌套了7个层次。 尝试重构此代码导致失败。 你遵循的每一条path都会带来另一个挑战,更多的path,然后是分支的path,然后回到以前的path,经过两个星期的低头重构,你会意识到,虽然也许封装得更好,新代码几乎和旧代码一样晦涩难懂,可能包含更多的错误,因为您重构的原始意图完全不清楚,不知道究竟是什么确切的商业案例导致原来的灾难,不能确定你已经完全复制了这个function。 进展几乎是不存在的,因为代码库的翻译几乎是不可能的,而某些无辜的事情正在重命名一个variables,或者使用正确的types会产生指数级的非预期的副作用。
试图改进上述代码库是徒劳的。 无论如何,重构通常会导致80%的重写,最终的结果远不及80%的改善。 你最终会遇到一些非常不一致的东西,而新代码有很多妥协,为了与遗留代码的互操作性而必须实现(其中一半是不必要的,因为新代码需要互操作的遗留代码后来得到重构)。 只有两条途径可以遵循……继续通过黑客进行“修复”和修改来累积技术债务,同时希望应用程序在它自己的权重下崩溃之前被弃用(或者转移到另一个项目),或者有人做出商业决定,并冒着彻底改写的风险。 我讨厌这两种select,因为这通常意味着等到事情发生失败或者项目进度落后,然后在接下来的三个月的晚上和周末,试图获得可能从来不应该活着的呼吸第一个地方。
那么,你如何决定?
- 现有代码的工作情况如何? 它是可靠的和相对无缺陷的?
- 我的团队中的人员是否能够以合理的努力理解此代码的function? 如果我带一位有经验的开发人员,他/她能够在合理的时间内充分理解这一点,从而提高工作效率吗?
- 做什么应该是简单的缺陷需要地质时间测量来解决; 以至于我们无法做出真正的改善或者不能完成项目的最后期限?
- 代码库如此脆弱以及预期的生命周期如何使应用程序适应未来预期的业务需求的能力是非常可疑的?
- 现有的代码是否真的达到了原来的function要求?
- 您的组织是否愿意投资应用程序,或者某人(特别是组织结构图上的某个人)是否将自己的问题交给他们?
- 你能提供财务或风险为基础的理由,由事实支持,为重写一个商业案件?
- 如果在全面了解重写的时间和成本后(包括开发适当的规格,质量保证testing,后期生产稳定性和培训),是否仍然有意义重新开始编写代码(我们开发人员往往只会考虑编码时间)?
- 你有select吗? 现有的代码甚至有可能满足要求(因为如果不是的话,重写大块将成为项目的一部分,并被认为是“改进”而不是重写)?
有一个古老的格言说:
没有坏的代码这样的事情。 只有代码可以做你想做的事情,而代码做不到。
知道何时重写的关键在于。 系统目前是做你想要的吗? 如果答案是肯定的,缓慢,但稳步的改善是你最好的select。 如果答案是否定的,重写就是你想要的。
回到Joel的文章中,他谈论的是那些杂乱无章的代码,但是可靠的软件能够提供预期的价值。 相反,如果你有不可靠的代码充满了重大的错误,并没有涵盖所有的用例。 你有那些应该在那里但没有工作,或者只是失踪的事情。 在这种情况下,所有生长出来的小毛发都不是bug修复,而是癌症。