软件重写
对你的“烂泥球”进行心脏搭桥手术
引言
一个“烂泥球”系统会持续带来痛苦,因为重写它令人望而生畏,而且失败的重写故事比比皆是。如果痛苦达到了让所有人都无法忍受的地步,那么是否重写软件的问题最终就会出现。本文讨论了
- 如何降低最终不得不重写软件的风险;
- 如果你正在考虑重写,那么开始重写的前提条件;以及
- 一旦你决定重写,如何提高成功的几率。
案例研究。本文大部分内容基于对一个走向取消的产品(product)的经验。它无法满足客户所需的吞吐量,并且客户希望添加那些难以实现的、新的功能。它进入市场较晚,需要增加市场份额,因此如果其吞吐量和开发人员的生产力没有显著提高,它很可能会被取消。因此,决定该产品必须重写。这次重写挽救了这个产品,该产品至今仍在使用,已经超过 20 年了。
重写之路
避免重写
与其他工程产品不同,软件系统的本质是变化。要求在“下一个版本”中将一座桥梁延长 50 米,这会引起嘲笑,但对于软件产品而言,这种期望是很常见的。
如果你的系统最初的设计没有预见到新的需求,那么添加它可能会很困难,因为它无法与现有架构干净地融合。如果你不断地将这些类型需求强行塞入你的系统,它就会退化成一个“烂泥球”。“技术债务”这个术语描述了这种情况。生产力下降。错误出现的频率更高,也更难修复。最终,人们会开始考虑重写。
重写通常可以通过重构来避免,重构是持续重塑软件以保持干净的整体设计的过程。在极限情况下,重构和重写是相似的。但重构是渐进的,它将成本分摊到产品的整个生命周期中,而重写是破坏性的,并在进行过程中会带来更大的开销。重构通常被视为演进式的,而重写将被视为革命式的。
尽管重构的重要性似乎越来越被接受,但它仍然常常遇到阻力。原因主要有两个:
- 经理们希望每个人都致力于新功能。
- 如果它没坏,就不要去修。
第一个原因是可以理解的,因为新功能会产生收入;而重构不会。然而,这是一种短视的观点,因为它只关注看得见的部分,而忽略了看不见的部分。看不见的是不重构的潜在后果:系统会退化到这样一个地步,即使每个人都在忙于新功能,但随着生产力和质量的下降,交付的功能却越来越少。
这并不是说重构总是至关重要的。如果客户现在需要修复一个错误,那么就“不顾一切”地去修复它,即使它影响了架构。但这应该在发布分支上进行。一个正确的修复,包括所有必要的重构,应该稍后合并到开发分支。
不进行重构的第二个原因——不想修复一个没坏的东西——也是可以理解的。“修复”可能会引入错误,而错误需要被发现和修复。而且因为它本身没有坏,修复它也被视为浪费时间,这些时间本应用于开发新功能。所以,重构仍然没有发生。一位前同事称之为验证惯性:一旦软件经过测试并已知可以正常工作,除非修复错误或开发新功能时不可避免,否则没有人愿意改变它。
为了克服验证惯性,自动化测试至关重要。开发自动化测试能力所花费的成本将很快得到回报。它将消除在每次软件发布前手动执行不断增长的回归测试集的成本。同样重要的是,它将通过使重新测试修改后的软件以确认其仍然正常工作变得容易,从而鼓励重构。
最好避免重写,所以你需要重构。要重构,就需要自动化你的测试。
案例研究。尽管该产品有自动化测试,但它是使用原型应用程序框架快速开发的。这帮助它进入了市场,但很快就发现添加新需求变得很困难。仅靠重构无法解决这些挑战,因此需要更积极的重写。
考虑重写
一旦一个系统变成“烂泥球”,它是否应该被重写?在承诺重写之前,你需要满足几个前提条件。
你的产品需求必须得到充分理解,并且你必须能够有效地确定新软件是否满足这些需求。
如果你的产品符合行业标准,例如 IETF 发布的一些标准,这将大大有助于满足这一标准。如果不是,你应该有一个在开发开始前编写的产品规范,以及描述后来添加的功能的其他文档。
如果你的产品需求没有被记录下来,你必须记录它们,并通过让熟悉产品的人员审查它们来验证它们。这些审查人员应该包括客户。在不知道需要交付什么的情况下开始重写,在临床上是疯狂的。
如果你的产品有一套好的测试,它们将有助于确认产品是否满足需求。如果测试不是自动化的,请将其计入重写成本。你之所以陷入困境,很可能是因为没有人愿意重构需要手动重新测试的软件。
案例研究。该产品有行业标准需要遵循,并且还拥有一套大型的自动化测试。
你的架构师必须确信重写将显著提高生产力。
如果你听到“什么架构师?”,那么这就是你陷入困境的一个原因。在找到一位在你所处领域有经验的架构师之前,就不要考虑重写。然后,架构师需要时间来研究你的产品需求和现有软件,然后再估算重写能提高多少生产力。
如果你的架构师表示,他们可以领导一次重写,从而显著提高生产力,请与你的高级开发人员一起审查设计的提案,看看他们是否普遍同意,因为让他们承诺进行重写非常重要。
你的系统越大,你可能就越需要一个应用程序框架来显著提高生产力。理想情况下,框架应该内部开发,以便它可以演进以干净地支持新功能。但是,如果架构师确信外部框架能满足你的产品需求,并且其供应商对用户响应迅速,那么也可以选择外部框架。
你的框架应该定义基类以及它们之间的许多协作。它实际上是你系统对象模型的体现。它的基类通常应提供默认行为,开发人员可以根据需要进行自定义。一个设计良好的框架
- 提供可重用组件,缩短开发时间;
- 消除不必要的差异,使开发人员更容易理解彼此的代码;以及
- 即插即用,允许在不修改现有代码的情况下添加新功能。
案例研究。架构师花了很多时间思考一个应用程序框架,该框架可以将生产力提高一倍,甚至可能提高三倍。更精确的估计无关紧要,因为如果重写失败或未能达到挽救产品的必要程度,该产品可能会被取消。
你必须在产品的生命周期内收回重写的成本。
要确定这一要求是否能得到满足,你需要回答一些问题。
重写产品后,开发人员的生产力会有多大提高? 假设生产力将提高 p 倍。为了收回重写成本,p 很可能至少需要 2,甚至更高。
开发待重写软件已经花费了多少人年? 假设这个数字是 k,那么重写成本应该约为 k/p。但这并不完全简单,因为当开发人员重写现有功能而不是开发新功能时,也存在机会成本。在此期间,你的产品产生的收入将减少。
如果重写(或不重写)产品,产品的预期寿命是多少? 重写一个即将过时的产品是不可行的。竞争对手是否有可能在重写完成之前取代该产品?或者重写能否显著延长产品的寿命?如果可以,那么它看起来非常有吸引力。
现在我们可以开始估算重写是否能收回成本。假设
- 在不重写的情况下,产品的预期寿命为 m 年,重写后为 n 年;
- 正在开发该产品的开发人员数量为 d;以及
- 重写需要 r 年。
我们还可以引入 f 来表示每人年交付的功能数,但这是不必要的,因为在重写之前,f 可以缩放到 1,重写之后则缩放到 p。所以我们只说,在重写之前,d 名开发人员每年产生 d 个功能,之后则产生 pd 个功能。
因此,不重写会产生 dm 个额外功能。重写在重写期间会产生 dr - k/p 个功能,完成后会产生 pd(n-r) 个额外功能。所以,为了让重写收回成本,必须满足
d(r - m + p(n - r)) > k/p
也就是说,因为重写而交付的额外功能必须超过其成本。
然而,我们做了一些并非必然成立的假设。
首先,我们假设收入与交付的功能数量成正比。这是值得怀疑的,因为在任何版本中,最低优先级功能的价值通常低于最高优先级功能。例如,交付的功能数量翻倍,不太可能使收入翻倍。
其次,我们假设开发人员数量是恒定的。然而,一旦有了应用程序框架,几乎肯定可以增加更多的开发人员。这是因为一个好的框架可以允许更多的开发人员并行工作,而不会互相干扰。
只要增加一个功能的收入大于增加一个开发人员的成本,交付更多功能就是有意义的。鉴于客户通常在一个版本中需要比能交付的更多的功能,这种情况通常是存在的。
如果你认为重写后可以增加更多开发人员,我们可以用 d0(重写前开发人员数量)和 d1(重写后开发人员数量)替换 d,在这种情况下,公式变成
d0(r - m) + pd1(n - r)) > k/p
如果重写是否合理仍然不清楚,请创建一个电子表格,通过估算每年的收入来评估这两种选择。为此,你需要一个高层项目计划,这将在下一节中概述。而且,别忘了还有第三种选择:停止新开发,将产品置于维护模式,并尽可能地榨取剩余价值。即使不再交付功能,产品在一段时间内仍会产生收入。
案例研究。该产品没有运行这个公式。很明显,它必须被重写,并且如果它能生存下来,它将会有更多年的开发。如果进行了分析,大概的数字会是
d0=50,r=1.5,m=3,p=2,d1= 100,n=12,k=80
将这些数字代入上述公式得出
50(1.5 - 3) + 2 x 100(12 - 1.5) > 80/2 ⇒ 2025 > 40
今天,n 的值已经达到了 22,尽管开发团队的规模(d1)现在小了很多,因为产品已经成熟并且功能请求较少。但这个公式本来可以加强重写的决定。
管理重写
本文不提倡具体的项目管理或软件开发方法。它只是假设你将继续使用现有流程,或者在认为有必要时采用新的流程。本文仅限于特别适用于重写的建议。
考虑品牌宣传。
为了获得重写的批准,最好称之为“再工程”,这听起来会更专业,对一些高级管理人员来说也少了一些可怕感。
限制范围。
尽量从你的“烂泥球”中重用一些东西。整个子系统可能适合新的架构而不会对其构成损害,特别是如果你为它们构建包装器。将重写限制在已经变成泥潭的区域,可以显著提高成功的几率。这就是为什么本文的标语使用了“对你的烂泥球进行心脏搭桥手术”的隐喻,而不是“一枪毙命”。
经理们会害怕重写,因此可能会试图过度限制其范围。因此,作为架构师,你可能需要淡化重写的程度。经理们最终会了解到真相,但那时已经太晚了,无法将重写引向正确的方向。
案例研究。该产品是在专有平台(proprietary platform)上构建的,其操作系统被完整地重用。产品配置和操作的几个子系统也是如此。重写主要集中在应用程序。其范围被淡化了,产品副总裁在最终真相大白时感到惊讶。
避免客户可见的更改。
如果你计划更改产品的操作方式——例如其 GUI——那么与操作它的人员进行咨询是必不可少的。不是他们的管理层,而是真正操作它的人。很容易陷入提供你确信更好的用户界面的陷阱,结果却遇到了那些已经知道如何操作你的产品并且不想重新学习的人的阻力。
案例研究。重写主要集中在应用程序,并重用了涉及产品配置和操作的子系统。这避免了引入客户可见的更改。
保持火车前进。
你可能已经注意到迄今为止未明确说明的假设:你的产品已经有客户。如果没有,你构建的是一个原型,而不是一个产品,所以遵循弗雷德·布鲁克斯的建议并将其丢弃。1 或者,如果现在进行营销太重要了,那么就承诺在初始发布后立即开始重写。如果你的产品如此具有创新性,它应该能够在你在忙于重写的同时,仍然吸引住客户。
客户总是想要新功能,所以如果你告诉他们你为了重写而暂停开发,他们会不高兴。他们甚至可能开始怀疑你的能力,并考虑更换你的产品。因此,至关重要的是,要不断为他们提供他们需要的重要功能,这些功能在重写期间也能带来收入。
因此,你需要将开发团队分成两组。一组将继续在现有代码库上交付功能,而另一组将从事重写工作。当重写重新实现了你产品足够多的功能后,将所有人都转移到新软件上进行下一个版本,在此期间,你将完成对任何缺失功能的重写,同时交付新功能。
案例研究。开发团队被分成了两组,如上所述,以便在重写期间仍能交付新功能。
分阶段构建。
在重写期间将开发团队分成两组,以便继续交付新功能,还有另一个好处。它防止你一次重写过多,这会显著增加失败的风险。
特别是当重写涉及开发应用程序框架时,需要时间来为广泛开发做好准备。因此,明智的做法是从小处着手,指派一小群熟练的开发人员负责重写。他们的任务是实现框架,以及一些跨越系统所有层和组件的功能。只有在存在稳定的基础之后,才应将一个更大的团队分配到新软件上。
案例研究。重写跨越了三个版本——大约 18 个月——涉及大约 50 名开发人员的团队。
- 在第一个版本中,六名开发人员实现了新框架和少量应用程序,而团队的其余人员则继续处理旧代码库。
- 在第二个版本中,团队的一半人员在新代码库上重新实现了现有功能,而另一半人员继续在旧代码库上交付功能。
- 在第三个版本中,整个团队都转移到了新代码库上,其中大约一半的人员仍在重写以前的功能。
每周举行一次设计会议。
作为领导重写的架构师,安排每周一次的会议,会议时间最长可达三个小时。会议的重点会随着时间而改变。起初,其主要目的是教近期加入重写项目的开发人员了解应用程序框架。之后,随着这些开发人员开始重写各种功能,他们会开始询问如何实现有挑战性的需求。有时,你会了解到框架设计中被忽略的一个需求。如果该需求再次出现,你必须认真考虑演进框架,以便能够干净地实现该需求。
会议从早上 9:00 开始。这有助于设定中午的结束时间,并让开发人员在下午进行工作,此时会议中的讨论仍然新鲜。并且将其安排在周三。没有人想在一周的开始就在会议中度过,而许多周二因为周一的假期而落入这个范畴。周五不行,因为如果每个人都在为截止日期工作,有些人就会休息。因此,周三是最好的,尽管周四也比较合理。
发布会议纪要,以便未能参加的人也能从讨论中受益。自己撰写纪要,因为纪要需要包含如何使用框架以及框架将如何演进以适应被忽略的内容的摘要。
会议可能会冗长而乏味,但这次会议有一个明确的目的——设计——开发人员很喜欢。它对你的好处是,没有它,你会被不断打断咨询。会议通过整合你大量的咨询时间并减少回答相同问题的次数,将中断减少到可管理的程度。它受到开发人员的欢迎,因为他们可以了解整个系统:他们会听到同事们在做什么,出现了什么类型的问题,以及如何实现具有挑战性的需求或实际演进框架以支持它们。
案例研究。在重写期间,尽管每周设计会议是可选的,但与会者很多。有时会开足三个小时,有时不到两小时就结束了。它基本按照描述进行。
摘要
重构以避免最终的重写需求。要鼓励重构,请自动化你的测试。
在决定重写之前,要确信
- 你的产品需求得到了充分理解;
- 你将能够有效地确定新软件是否满足这些需求;
- 你的架构师知道重写将显著提高生产力;以及
- 你将在产品的生命周期内收回重写的成本。
在你决定重写之后,通过以下方式提高成功的几率:
- 限制重写的范围;
- 避免客户可见的更改;
- 继续在现有代码库上交付功能;
- 分阶段构建新软件;以及
- 安排每周一次的设计会议。
注释
1 "Plan to throw one away; you will, anyhow." 在 《人月神话》,1975 年(第一版),第 116 页。请注意,布鲁克斯使用 architect 来表示产品架构师,他负责指定系统的功能和行为,而本文将其用作软件架构师,负责确定其高层设计。
历史
- 2020 年 11 月 1 日:初始版本