技术债被广泛视为一件坏事,它应该避免或者要尽快进行偿付。
你应该这样做吗?我们并不这么认为。首先,我们对比了技术债与财务债,阐述了它与战略设计(Strategic Design)的相似性以及它的利益相关者。然后,我们列出了识别代码中技术债的各种可行的方式,这可能是你所关心的。
最后,我们描述了项目中可以偿还技术债的不同方式,并且阐述了当你在决定要偿还债务、转移债务或者只是支付利息哪个方案更好时,必须要考虑的因素。
什么是技术债
开发人员在实现新特性的时候,有两种不同的方式:一种是快速且混乱地完成,这会使得将来的变化很困难。另一种是整洁(clean)和明智的方案,它需要更长的时间来实现但是将来的变化会更为容易(亦可参见 Martin Fowler )。不过,如果同一个特性以较为凌乱的方式实现时,能够具有相同的功能和较低的成本,那么项目的赞助者为什么会接受一个较高成本的整洁实现呢?他们为什么会花费金钱在自动化测试覆盖率上呢?测试不是什么特性,因此不会交付业务价值!
可能凌乱的代码和没有测试的代码也能够交付期望的业务价值,对于客户来说运行得也很好,但是这将会导致难以控制的代码基,极有“个性”(贬)的开发者会搞出一些缺乏灵活性的产品出来。大量的混乱代码有可能会使得整个工程部门停滞(stand-still)。
“技术债”的隐喻说法——与“财务债”的相似和不同
1992 年,在与非技术的利益相关者沟通这个问题时,Ward Cunningham 第一次使用了“技术债”这个隐喻说法。低质量的或没有自动化测试覆盖的代码可以与财务债进行类比。这样的代码就像财务负担,它会将所有的利益相关者牵连进来——并不仅仅是开发人员——债务会导致将来要支付的利息。而本金就是将代码基重构为整洁的设计所付出的代价,整洁的代码会简化将来所发生的变化。如果团队将来工作在混乱的代码基之上,相对于良好的代码基所付出的的额外成本就是利息。
与财务债的不同之处在于,你不一定必须要偿还技术债。有时候将其偿清并没有太大的意义:有些代码是很少甚至从来不会阅读或修改的。因此,技术债也需要考虑发生的概率——混乱的代码将来要接触到的可能性和频率如何?另外一个与财务债的不同之处在于,偿还这个债务的人并不一定是技术债的最初创造者,而是随后维护代码基的开发人员。
类似于财务债,技术债未必是一件坏事。如果你知道如何偿付的话,举债买房也是一种负责任的行为。但是,如果明明知道无力支付账单,依然通过信用卡购买大量的奢侈品,那么通常会导致灾难性的后果。软件的技术债可能会在提前释放版本方面带来优势,组织的收益可能要超过偿还技术债的成本。在财务上,如果本金加利息低于投资的收益,那么举债是一件好事。对于软件来说,这依然是成立的。如果你牺牲了内部质量来谋取市场第一的地位,那么相对于市场中稍微靠后的位置但是具备较好的内部质量,你需要获得更高的收益,只有这样才是值得的。但是,这里会有风险,因为很难预先估计这些收益,这里会有一定程度的不确定性。
技术债与战略设计
Eric Evans 所提出的战略设计理念为我们处理技术债提供了一个思路。战略设计(Strategic Design)表明在一个系统中,不可能在系统的所有地方都保持高质量。因此,团队就有两种选择,要么对系统中各部分的质量高低听天由命,要么主动地对质量进行控制。很少会被读到或接触到并且没有实现重要需求的混乱代码并不需要绝对得完美,因此我们没有必要花费大把的工夫将其重构为好的代码。所以,问题在于哪一部分的代码要具备高质量?某个实现如果设计很糟糕,很可能不会有好的质量——除非这部分实现并不需要好的设计。这种观点当然是有问题的:如果代码的每一部分都不需要好的质量,那么最终你所创建的将是一个无法维护的系统。
理解战略设计和技术债能够使得项目内部进行更好地沟通,因此软件项目的利益相关者就能做出更好的决策。开发人员也会意识到如果都要付出巨大努力的话,其实并不是所有的需求都需要非常优雅地实现。客户会理解快速且脏乱的解决方案将会导致债务的出现,而这最终是要偿还的。技术债包含了隐藏的技术问题,这就像冰山一样,一旦出现就会导致项目的失败,比如出现大量的缺陷,就成本收益来说,这些质量问题解决起来可能太迟了。
技术债的利益相关者
技术债涉及到软件项目中的众多利益相关者:
- 客户(Customer)将会因为低生产率导致的缺陷或功能缺失而恼怒。
- 这会导致客服(helpdesk)的额外成本,因此也会导致那里的人产生不满。
- 对于市场部门(marketing)来说,增加的开发时间和质量问题也是一个麻烦。
- 缺陷会导致频繁地打补丁,这会引起运维团队(operations team)的厌烦。
- 众多带有恼怒情绪的当事人显然不会让管理人员(management)觉得很舒服——尤其是当缺陷、延迟或安全问题产生很坏的影响时。
- 最后一项但同等重要的是开发人员(developer) 也会因此遭罪。没有人愿意交付糟糕的成果。另外,没有人愿意接手别人的糟糕成果。
技术债是不可避免的
但是,众多的项目为什么不在开始的时候就把事情做好从而避免技术债呢?假设开发人员并不懒惰同时对相关的技术和模式经验丰富,那么技术债基本上都是因为时间压力所导致的。开发人员可以借助快速而脏乱的实现获取较高的短期生产率,同时降低当前释放版本的成本——这实际上会导致将来的版本具备更低的效率和更高的成本,他们对此是心知肚明的。但是,团队永远不能确定当前的释放版本真的能够获取较高的生产率和较低的成本。可能的情况是,采取捷径产生的回力镖(此处的原文是 boomerang,指的应该是采取短期方案所产生的问题提前爆发了——译者注)可能回来地比预期要早,因此团队要早于预期就支付利息。
但是,我们作为开发人员其实并不想在质量上有所损失或妥协。有时候,外部的条件让我们必须要快速完成版本释放,因为如果不这样做的话,我们就永远不会有下一个版本了。或者团队假设这些代码不会经常被读到或修改。前面所讨论到的“战略设计”明确允许在质量上的妥协。在那种情况下,好的代码可能就显得有些“画蛇添足(gold-plating)”了,这样做有些过犹不及。
因此,我们可以看到技术债的隐喻说法能够帮助所有的利益相关者对糟糕质量的问题进行交流,否则的话,这些问题可能一直得不到注意,而意识到的时候就已经太晚了。糟糕的代码质量以及质量的持续退化所带来的危害通过利息这个隐喻也能很好地得以展现。但是,这个隐喻说法也有些欠缺:并不是所有的技术债都需要进行偿付。团队也不能明确知道债务有多少以及何时需要偿还。欠下债务的人并不一定是要偿还债务的人,通常会是另外一个人。现实生活中的事情是难以预料的!
识别技术债
技术债的一个主要问题在于它并不是显而易见的。通过账户信息,任何人都能看出财务债的数额。但是,一个团队怎样有效地识别技术债呢?有哪些指标呢?
- 团队成员在自己的言语中所表现出的常见“坏味道”:“能够修改这块代码的只有 Carl”、“让我们复制并粘贴这段代码就可以了”、“如果我动这块代码的话,那所有的地方就都不好用了”或者代码中有太多的 TODO 或 FIXME( Nico Zazworka 很好地描述了这一点。)
- Scrum 团队所保持的速度。如果团队或外部环境都没有发生变化,但是速度降低了,这可能意味着软件系统有过多的技术债。
- 软件的寿命。技术债的一个指标就是系统使用了非常旧的库,这些库已经不再维护了或者新版本有更高的生产率(如 EJB2 VS EJB3)。在最坏的场景下,所使用的库太陈旧了以至于这些库的开发人员都不再支持它了。
- 自动化测量技术债在一定程度上是可行的。正确地配置像 Sonar、SonarJ 或者 Structure101 这样的工具能够发现严重违反最佳编码实践的地方。正如这篇博客所言,这并不是终极的解决方案,却是一个很好的方案。借助于 Sonar,你能确保开发人员遵循了重要的代码指标,比如适当的类和方法规模以及较低的循环复杂度。像 Structure101 这样的工具会发现结构化的问题。比如循环依赖。如果两个元素互相依赖,任何一个元素的变化都会潜在地影响另一个元素。这两个元素必须要一起发生变化——尽管它们实际上应该进行分离并独立开发。此外,Structure101 还会发现复杂的代码:它所关注的就是元素之间的依赖。如果一个系统的类或包有太多对其他类或包的依赖,那么这个系统会很难理解和维护。如果开发人员想要做一些变动,他必须要理解很多的类和包并且还要考虑很多依赖。这些问题其实就是架构中最主要的挑战。
- 代码覆盖度工具能够探测出自动化测试到底覆盖了多少代码。这个指标使用起来要当心,因为并没有通用的指导原则。一般来讲,超过 90% 的覆盖率就是就是一个好的信号,它标志着有足够的测试用例。相反,低于 75% 的覆盖率就标志着会有严重的问题。
- 但是,在很多场景下系统中有技术债,却并不能直接通过代码测量出来,比如笨拙的解决方案、选择了错误的技术、解决方案的设计虽然执行得很好但设计本身很差、不容易被工具检测到的有害的 hack 方式。在这些情况下,技术债必须要以不同的方式来进行管理:每个释放版本的缺陷都在快速增长、工作速度在持续降低或者团队在接近版本释放的时候面临特别大的压力。
- 一个很坏的指标就是在生产环境经常出现问题。这意味着系统中的问题非常广泛以至于无法进行可靠的操作。
所有的这些指标都是可测量的,但是并不一定都是在代码之中产生的。这些问题越早解决,带来的成本越低廉。如果开发人员发现了技术债并对此进行了交流,但是没有做什么针对性的事情,那么业务人员也将感知到这些问题。这个陈述清楚地表明:技术债并不是那些想编写漂亮代码的开发人员的噱头。它是真实的成本元素并且可能会成为项目的风险。因此,技术债必须可见并且可管理。在现实生活中,债务不一定是什么坏事,但是必须要慎重并恰当地使用。这意味着对于软件项目来说,偿付技术债完全是一个业务决策。它绝不是开发人员就能做出的简单决定。
如何管理技术债?
我们已经看到了,技术债不能视而不见。即便是客户那一边的非技术管理人员也必须要对技术债的管理保持关注,从而尽可能地保持短期、中期以及长期成功之间的平衡。一个团队如何既能避免把时间浪费在不重要的美化上,又能做出有意义的业务决策从而提高其代码质量呢?
简单粗暴地说法,如“什么都不做”或者“如果代码不能再维护下去了,那就开发一个新的”,并不会提供什么帮助。
我们在本文中考虑了两种有前景的方式,在多个项目中已经发现它们很有用处了:
- 技术化的 Backlog(Technical Backlog)
- 在需求评估的时候包含技术债的成本
- 在此之前,我们想要讨论在特定的上下文中很有价值的两种其他重要方法:
- 针对重构的缓冲任务(Buffer-task)
- 清理类型的释放版本(Cleanup-release)
当讨论技术债的时候,通常会有一个绕不过去的问题需要回答:技术债一定要进行偿还吗?Frank Buschmann 描述了 3 种策略,我们稍后会进行讨论:
- 偿付债务
- 债务转换(debt conversion)
- 只支付利息
缓冲任务(Buffer-task)
团队在每个释放版本中都创建一个缓冲任务,例如它占用可用时间的 10%。团队成员可以将没有预定的重构时间记录在这个任务上。所以,它预留给未知的问题,而这些问题在将来会出现。这样的缓冲任务很容易安排和使用。但是,这也会带来一定的风险,那就是时间被浪费在了不重要的工作上。缓冲任务并不强制任何人考虑他们是不是将时间花在了有用的重构上。开发人员只是记录花在缓冲任务上的时间。最可能出现的情况是缓冲的时间并没有最好地加以利用——尤其是确定要做哪一项重构,尽管这应当是业务上的决策。令人遗憾的是,使用缓冲任务就意味着要做什么没有被真正地定义。
清理类型的释放版本(Cleanup-release)
有些团队会不时地发布技术化的版本(technical release)来提高代码基的水平。如果已经列出了有必要重构的地方,那这种方式才会起作用。否则的话,团队会有风险将时间浪费在并不重要的重构上。这种方式也必须要得到业务方面的支持,因为它可能会延误新的特性。当然,这需要业务人员理解技术债。如果有些地方需要付出特殊努力的话,你应该考虑一个纯粹的清理版本来对代码基进行清理并对架构进行修订(rework)。例如,如果相同部分的代码在开发和运维期总是导致问题的出现,那么现在的架构可能已经不适合当前的需求了。这样的问题不能通过小的重构来解决。清理版本允许进行大量的变化。
在非常匆忙和时间要求比较紧张的释放版本之后,发布清理版本是不合适的,这样的版本会有大量的技术债。对新的代码基只有很少的经验,所以没有人能够清楚哪部分代码需要提高。存在一种风险那就是修改了那些本来没有提高必要的代码。
技术化的 Backlog(Technical Backlog)
技术化的 backlog 是一个既定的最佳实践,它用来定义纯粹的技术化工作。这种目的的任务会创建在任务跟踪或需求管理工具之中。每项任务会有一个简短的描述,包含了要做的技术变化、对于项目来说这些技术变化为何是重要的以及变化提高了哪一部分的代码。类似于其他的任务,我们还要评估开发足够好的解决方案需要多长的时间。除此之外,还要评估已有的代码会支付什么样的利息。精确的评估是很难的,但是通常粗略的估计如“较小”、“中等”或者“较高”就足以指导做出决策。最后我们还需要一个概率:这些代码在不远的将来被读到或修改的可能性?
这种方式有多项优势:
- 对每个人来说,技术都是可见和明确的。基于对难度的评估以及将来代码发生变化的影响,在是否以及何时完成重构任务方面,可以取得一致的决策。
- 每项任务的成本可以很容易地跟踪。
- 技术化的任务和特性任务间并没有混合。
- 但是,这种方式也有一些不足:
- 客户必须要确定 backlog 和任务的优先级。客户只有在一定程度上关注技术问题才能做到这一点,因为如果他不了解软件细节的话,就不能确定技术化任务的价值。
- 另外,客户可能也不能完全理解纯粹的技术化任务所带来的业务收益。在很多情况下,这里会存在一定程度的不信任感,那就是为什么一项与任何特性无关的技术化任务是真切需要的。
依我们来看,只有在一些特定的场景下,客户才能够进行决策,如软件更新。对于大多数客户来说,很显然过时的软件会导致问题的产生。是否要进行一项更新具体取决于成本、风险以及更新的必要性。定期地更新像 Java 运行时环境(Java Runtime Environment)或 O/R 映射这样的组件通常不会太费事也不会有太大的风险。非常广泛的修改如替换 Web 框架或者将关系型数据库变更为 NoSQL 解决方案必须要有业务上的理由,比如应用的性能或用户体验必须要提高。为了达到这样的目的,技术化的 backlog 也会是很合适的。但是对于这样的任务你可能会初始化一个单独的项目,这取决于预期所付出努力的程度。
如果有一个新的需求要基于已有的代码基进行实现,但是这个代码基对新的需求来说设计得并不好,在这种场景下技术化的 backlog 就不是那么合适了。在这种场景下,在实现需求之前可能需要一个重构。为了做到这一点,在评估需求的时候,必须将重构所需要的额外努力考虑进去。
考虑技术债的需求评估
战略设计告诉我们并不是代码基的任何部分都需要很好,只有那些可变性会带来业务价值或者因为其他原因经常发生变化的那些部分需要如此。所以,很少或从不发生变化的技术债代码可以忽略。同样重要的是:如果没有好的原因,代码不要进行重构。所以,如果一个新的需求会带来切实的业务价值,那么我们就不要基于写得很糟糕的代码实现这个需求,而是要预先进行重构。按照这种方式,真正非常重要的需求能够达到“足够好”的标准。当团队在将来的释放版本中要实现这样重要的特性时,应该将重构预算进去然后才是特性实现。当然,这也有例外的情况,我们将会在“偿付还是不偿付”部分进行讨论。
这样的过程能够确保没有不必要的美化工作并且重构都是直接与需求相关的。这样客户就能与团队一起讨论并确定重构的优先级。客户也能决定一个需求是不是要基于债务和利息来实现。可能还会有其他的因素要考虑,如上市时间。客户可能也会明白基于这些需求,代码未来会发生的变化。为了节省将来的成本,他可能因此决定将其重构为整洁的代码。
在实现的过程中,如果团队遇到代码基的问题,那么将会产生新的问题。此时,团队必须要决定怎样做:可能按时交付承诺的功能是非常重要的。那么,就必须要接受因此导致的技术债并支付利息。或者,基于糟糕的代码来进行所有的开发会被视为一种浪费并且没有时间截止点的压力。那么,团队应该安排重构,这会导致一个稍晚的发布日期。这只能取决于环境尤其是客户的需求。
偿付还是不偿付
我们已经看到只有代码的重要部分必须是要整洁的。所以,代码应该只有在必要的时候进行重构。但是,对于特定的场景很难确定什么样的解决方案是最佳的。Frank Buschmann描述了 3 种策略:
- 偿还债务:将那些被视为技术债的代码、框架或平台进行重构或替换。
- 债务转换:将当前的解决方案替换为“好的,但并不完美的”方案。这个新的方案会降低利率。如果完美的方案需要特别高昂的代价才能进行构建的话,这可能是一个较好的选择。
- 只支付利息:与这些代码共存,因为相对于使用这些不太好的代码,重构的代价更为高昂。
开发人员和客户必须要基于成本、风险以及紧急程度来进行决策。总而言之,重要的重构应该是业务决策。关于支付利息,另外一个重要的方面就是:软件的生命周期结束时间(end-of-life)——如果一个系统马上要重建或下线的话,那重构还有什么价值吗?
结论
技术债可以被视为一个捷径,它可以在当下节省团队的时间、努力程度以及 / 或金钱,但可能会导致将来成本的增高。在软件项目中,技术债并不能真正避免。如果处理得当的话,它并不一定是坏事。
做到这一点是很困难的:糟糕代码给未来需求所带来的影响很难甚至不可能进行预估。但是,完全忽视技术债会给软件带来灾难性的影响,它应该保持可管理的状态。我们已经展现了多种不同的方式来应对技术债。每种可选的方案只能用于特定的环境上下文中。接受以下几点是很重要:
- 技术债会始终存在
- 技术债并不一定总是坏事
- 并不是在任何的场景下都必须要对技术债进行(完全的)偿付。
任何的偿付行为都应该是业务决策。即便这个偿付很小,以至于没有必要与客户进行磋商,开发人员依然要反问自己,相对于花费的时间和努力,所带来的代码提升是不是值得。
本文的英文原文下有不少有价值的评论,感兴趣的读者可以移步查看。——译者注
关于作者
Sven Johann是 Trifork Amsterdam 的软件开发人员。Sven 是 XP 实践的狂热采纳者,如结对编程、TDD 以及小版本释放(small release)。目前,他正在基于 Trifork 的 QTI 引擎为瑞士以及荷兰的学校开发在线评估软件。
Eberhard Wolff是德国 adesso AG 的架构师和技术管理人员。他关注于 Java、云计算以及软件架构。他是一些国际会议的固定撰稿人并且是多本图书以及文章的作者。
原文链接: Managing Technical Debt
评论