代码清理

使用敏捷方法清偿技术债务

David Laribee

在每一份基本代码中,都有令人生畏的黑暗角落。有的代码可能脆弱无比;有的代码可能产生回归 Bug 事后找麻烦;而在尝试理清有些代码时会让人抓狂。

Ward Cunningham 将代码中不易更改却容易出错的部分与金融债务做了形象的类比。技术债务让您停滞不前、创收受阻、盈利维艰。与现实世界一样,有低价债务,比如利率低于低风险金融工具利率的债务。也有高价债务,比如会让人债上加债的高利率信用卡费用。

技术债务是个累赘:它会扼杀生产力,使维护工作变得麻烦、困难甚至在某些情况下无法实施。除了使经济明显下滑,技术债务还会带来切实的心理负担。任何开发人员都不想早上刚在计算机面前坐下,就知道将要面对无比脆弱和复杂的源代码。由此产生的沮丧与无助感往往是更多系统性问题的根源,如开发人员流动频繁 — 而这只是技术债务的实际经济成本之一。

我所处理或检查的每份基本代码都包含一定程度的技术债务。有一类债务危害程度不大:在您系统中某些稳定且鲜有修改的角落,各种命名怪异的类型之间存在的错综复杂的依赖关系。另一种是虽然可立即轻松解决、但因开发人员忙于解决高优先级问题而常常忽略的粗心代码。此类债务的示例还有很多。 

本文概括了处理高利率债务的流程和一些策略。我要详细讲述的过程和做法非我所创,而是从精简/敏捷战术中照搬而来。

债务清偿案例

在我看来,“是否该清偿技术债务”这样的问题根本不是问题:答案当然是肯定的。技术债务会逐渐减慢您的前进步伐,因而阻碍您实现目标。有一种非常著名的更改成本曲线(见图 1),其中显示了 100% 质量测试驱动方法与“怀揣胶带边漏边补的冒进方法”之间的区别。

图 1 更改成本曲线

如更改成本曲线所示,简单易行的高质量设计可能初始成本较高,但产生的技术债务较少,因而随着时间的推移,对代码进行后续添加或修改的成本相对较低。在质量曲线(蓝色)上,可以看到初始成本较高,但长期的成本是可预测的。“冒进”曲线(红色)的进入成本较低,但后期开发、维护以及产品和代码的总体拥有成本都会越来越高。

Ward Cunningham 的编程第一定律 (c2.com/cgi-bin/wiki?FirstLawOfProgramming) 就是“降低质量等于延长开发时间”。

“开发高质量软件所需的时间有一个下限。如果您的代码尽可能简单,测试全面并且设计合理,那么对代码的添加和更改都会最快实现,因为这种情况下的更改只产生最小的影响。因此,如果率意编码,那么编码越多行进越慢,因为每一行代码的添加和更改成本都在增加。”

简言之,技术债务会逐渐使整个团队的吞吐量越来越少。

软件开发于我的乐趣在于享受纯粹生产力的感觉。缺少这种感觉甚至具有相反的感觉是令人痛苦的,至少对我如此。效率不高时我会很苦恼,剥夺我的潜在生产力也就是我的“工作好心情”也令我沮丧。在软件开发中有太多痛苦来源,但刻板混乱的基本代码当数其中之最了。这一心理效应会使团队士气遭受重创,反过来又使生产力恶化。

系统思考

为了清偿技术债务,需要从利益相关者和团队成员那里都争取到支持。为此,您需要进行系统的思考。系统思考是长远思考,也是投资思考。这种思考方式的出发点是:用今天的付出换回未来可预见的持续发展。

也许用一个比方解释系统思考最为方便。我家住佐治亚州亚特兰大市中心一个名叫 Inman Park 的住宅区,那里虽然不大,却古朴别致。我在那里过得基本上都很开心。不过,那里看来毫不讲求城市规划的布局让我多少有些不爽。亚特兰大的街道属拜占庭风格,像迷宫一样,很容易让人抓狂。倘若错过路口,想原路返回可没那么简单。如果作此尝试,您会发现眼前道路四面八方,不知每条路通往何处。这里本该是世界上美好的一角,道路规划却偏偏如此,让人摸不着头脑。

相比之下,纽约曼哈顿的街道可谓井然有序,至少大部分如此。此番景象不禁让人联想设计者或许是一位海军陆战队教官。各条主干道从南到北贯穿岛屿,一条条街道分布在主干道之上,形成整齐划一的分割线。不仅如此,主干道和街道都以数字序号命名:第一大道、第二大道、42 街、43 街等等。这样的设计让您即使迷路也不会错过一个街区。

两相比较,造成亚特兰大和纽约此方面差异的根本原因是什么?

亚特兰大的街道是牛踩出来的。您没听错,是“牛径”。随着在市中心与郊区频繁往返需求的逐渐增加,某牛仔在某日突发奇想:“咦,把这些牛径修成马路岂不方便?”

对于纽约这个纽约州不断发展的最大城市,州议会对于城市规划可谓深谋远虑。他们选择了网格规划,根据预料的结果建设井然有序的街道。他们为未来做好了打算。

这个故事道出了系统思考的精髓。虽然立法过程较慢,但在时间和精力上的投入会在“系统生存期”内实现最大的回报。虽然您要应付曼哈顿穷街陋巷中疯狂的出租车,但只要花点时间,您总能找到正确的路。而在亚特兰大,迷路是家常便饭,每天我都在感谢负责 GPS 全球定位系统的每个系统思考者。

产品重于项目

让开发团队开发项目,完成后甩手扔给维护团队的想法有着根本性的缺陷。请不要搞错,您制造的是产品,产品若成功将会存在很久很久。

如果您做过一段时间的专业开发,不用很久,只要两三年,就很可能体会过压力渐增效应。您开发一个软件,注定不能持久使用,不复杂,也没考虑更改需求。那么,六个月后,您要怎么办?修改?扩展?修复 Bug?

有时,有用的软件习惯于长时间缠着您,不肯离去。有两种生活写照,选择由您自己:您是愿意照看美丽的加州红杉林,看着它们生机盎然经久不衰拔地而起直冲云霄,还是甘心让无情的野葛藤蔓遮天蔽日让您的森林陷入无尽黑暗?

基本工作流

至此,我希望您已明白技术债务会对您的心理健康和客户利益产生重创。我还希望您能认识到对所创建的产品要有长远眼光的必要性。

现在,我们来探讨走出这一困境的方法。

无论在何种环境下,我都认为解决技术债务问题(实际上对任何改进均如此)的基本工作流是可重复的。从根本上来说,需要做四件事情:

  1. 找出债务的位置。每个债务项对您公司的收益和团队生产力有多大影响?
  2. 构建业务案例,并对债务影响领域的优先级达成共识(包括团队和利益相关者)。
  3. 采用行之有效的策略修复选择处理的债务问题。
  4. 重复执行。返回步骤 1,找出其他债务并保持您所做的改进。

值得告知软件过程迷的是,此工作流改编自 Eliyahu Goldratt 所创名为“约束理论”(ToC) 的业务管理方法 (goldrattconsulting.com)。ToC 是一种系统思考模型,为提高系统总体吞吐量提供了一个框架。这是一种粗略的简化,但 ToC 的前提理念是系统(例如,生产设备)生产力取决于其最大瓶颈。价值(如功能请求、汽车或任何可售产品)的实现要经历设想、设计、生产和部署过程。可由客户(在内部或外部)请求功能,该功能经过您的业务流程(系统),从设想变为有形结果。这些功能堆积在您的质量保证团队面前会怎样?开发要求若超出开发团队能力范围会怎样?您遇到了瓶颈,整个系统将慢下来。

您的基本代码中很可能有很多债务区域,也就是很多瓶颈。找出对您影响最大的债务将对提高吞吐量产生最大的净效应。以团队(系统)的形式研究、处理债务并进行改进是实现正面更改效应的最有效方法,因为更多人手关注代码就等于更低的风险和更好的设计。

确定债务区域

能够指出问题区域是非常重要的。如果没有在 wiki、共享列表或代码注释中跟踪债务,那么第一项任务就是找出债务。

如果采取团队工作的方式,建议召开会议制定出代码中顶级债务区域的具体列表。列表详尽与否并不重要。工作重心是找出重要的债务。此次会议可为作为团队领导的您提供第一次形成共识的机会。债务应获多数成员的同意和理解方可列入列表。

应将做好的列表持久保存。创建一个 wiki 主题,将其写在白板上(在一角显著注明“勿擦”),或根据具体情况选用不同的方法。您所选择的媒介应可见、持久并易于使用。该媒介应位于显眼的位置,经常进入您的视线。您需要复查列表并进行研究。人的短期记忆力有限,因此我建议保持一份包含五到九个难点项目的列表。不必过于担心遗漏列表项,重要的列表项一定会再次显现(如果它们的确重要)。

使用指标找出问题区域

寻找债务有时难度很大,特别是对新接触基本代码的团队而言。在没有集体记忆或口头传统可利用的情况下,可以使用如 NDepend (ndepend.com) 之类的静态分析工具探查代码寻找更多问题点。

工作充其量作为辅助,甚至作为第二选择。工具不会告诉您该如何去做,但可为作为决策参考。没有唯一的代码债务指标,但终日忙于某产品的人必然可以指出导致最大痛点的问题所在。静态分析工具可以告诉您哪里有实施债务。遗憾的是,由于命名、可发现性和性能方面的缺陷以及其他更多定性设计和体系结构方面考虑不周,无法通过静态分析工具得知哪里有债务。

了解测试范围(如果有测试)是发现隐藏债务的另一有力工具。显然,如果系统中有一大部分缺乏有效的测试覆盖,又如何能确定某项更改不会对下一版本造成严重影响?可能会产生回归 Bug,因而造成质量保证瓶颈、潜在的困扰,以及由于客户发现缺陷造成收入损失。

使用版本控制系统的日志功能生成上月或上两月的更改报告。找出系统中活动最多、执行更改或添加操作最多的部分,仔细检查这些部分是否存在技术债务。这有助于找出现在困扰您的瓶颈;修复系统中极少更改的部分中的债务没有多大价值。

人力瓶颈

如果只有一位开发人员能够处理某个组件、子系统或整个应用程序,就会遭遇瓶颈。如果存在代码单人所有权和只有一人了解的知识孤岛,那么当这个人离开团队或忙于其他工作的时候,产品就会难以交付。“应收帐款是由 Dave 负责的”,但 Dave 没有,有的是痛苦的记忆。如果找出项目中存在单人所有权的区域,您便可以考虑改进设计以使他人分担相应工作的益处和范围。这样,便可以消除瓶颈。

集体所有权的极限编程实践可以带来巨大的收益 (extremeprogramming.org/rules/collective.html)。使用集体所有权,团队中的任何开发人员都可以更改基本代码中的任何代码来“添加功能、修复 Bug、改进设计或重构代码。没有人会成为妨碍更改的瓶颈。”

呀!又是“瓶颈”!通过采用集体所有权,可以消除系统中只有一名编程人员了解的黑暗部分,从而不致出现一人出事满盘皆输的状况。集体所有的基本代码风险较低。

根据我的经验,这样的基本代码在设计上也更好。两个、三个甚至四个臭皮匠一定能好过一个诸葛亮。在集体所有的基本代码中,团队设计思维将取代个人特质和怪癖。

我将代码集体所有权称为一种实践,但集体所有权其实是运作良好的团队自然散发出来的优秀品质。试想一下:你们当中又有多少人在展示和处理“自己的代码”,而不是整个团队共享的代码?软件开发中常讲的团队实际就是工作组,其中有一位分配编辑,根据过去谁负责某一特定功能、子系统或模块安排编程任务。

作为团队区分优先顺序

我曾经说过,整个团队一起努力改进非常重要。作为敏捷开发教练,我的观点倾向于“有贡献,才有支持”。如果没有决定性的支持,那么追求不断改进的团队作风很难培养,更不用说持续了。

达成共识是关键。您希望大多数团队成员都支持您当前所选择的改进计划。我曾借用 Luke Hohmann 所著“Innovation Games” (innovationgames.com) 一书中的“购买功能”方法取得一定的成功。我将尝试对该游戏进行一些粗略的简化,同时建议您也了解一下这本书,看书中所讲是否与您的工作环境相似。

  1. 生成一份待改进项简表(5 至 9 项)。理想情况下,这些项目可在短期计划中完成。
  2. 按难度区分这些项目。我喜欢用 T 恤尺码的抽象表示法:S、M、L 或 XL(有关此实践的详细信息,请参阅“估计改进机会”侧栏)。
  3. 根据功能大小为它们定价。例如,S 号项目可能值 50 美元,M 号项目值 100 美元,依此类推。
  4. 给每人一定数量的资金。这样做的主要目的是在游戏中引入稀缺性。您要让人们须集资才能买到他们感兴趣的功能。比如,可以将 M 号功能定为没有任何个人能够买得起的价格。因为您正在争取共识,所以让不止一人理解这一重点非常有价值。
  5. 做一个二三十分钟的简短游戏,让大家各抒己见,参谋讨论。此时可能场面混乱却又不乏趣味,您可以借此发现团队中有影响力的人员。
  6. 检查卖出的项目以及卖出价格。您可以选择按已购买的功能对列表排序;或者,更好的做法是结合“购买功能”游戏的结果与其他方法(如下一版本计划认知)对功能排序。

估计改进机会

我曾提到使用 T 恤尺码大体估计债务项目或改进机会。这是敏捷开发方法中常用的手段。其目的在于按相对大小收集事项。按规模大小分为大、中、小几类。

在此并不需要力求精准。请记住:大小只是相对的衡量,并不表示付出努力的程度。您若想大体了解一下难度,有一个规律,就是在估计一定数量的项目以后,平均难度开始逐渐显现。即使某一中号项目实际上需要两名开发人员花两周时间完成,而另一个需要一个月完成,但总可以平均得出一个中号项目需要大约三周时间完成。

但是,时间一长,您会收集到大号或小号项目的好例子,这将有助于您将来进行估计,因为您有了比较基础。我以前用过不同大小号的几个示例,用它们估计新工作批次起到了良好的效果。

但管理层可能无法欣然接受这样的估计结果。他们开始会希望知道工作所需的确切时间,老实说,您可能需要投入更多时间去进行更精确的时间估计。

销售计划

现在您已经有了一个计划,是时候与项目发起人沟通削减债务的价值了。在实际运作中,这一步可以与债务认定工作同时进行。应从一开始就让客户参与进来。毕竟,制定计划需要时间、精力和(最终)资金。您一定希望不惜一切代价,避免出现有关制定详实的计划花费了谁的时间和金钱这样的提问。

任何消除大量债务的成功且持续的工作都绝对需要项目融资人和发起人的支持。签单的人需要了解您所做投资的用途。这对您可能是一项挑战,因为您要说服他人将眼光放长远,摒弃现在“先消费,后付款”的思维模式。仅凭一句“只是因为”显然不能过这一关。

这样做无法回避管理层必然要问的一个问题:“你们不是专业人员吗?怎么搞出这么多债务?”面对这样的追问,您可能感觉遭受当头一棒!毕竟,您作为一名专家,领着薪水,不就是要按照时间和预算拿出高质量的产品吗?

这是一个很难反驳的论点。我要对您说,切勿烦恼。请鼓起勇气,坦诚说明事实。这种做法看似风险很大,但可因人与人之间的责任感和信任予以化解。

可以这样表达您的论点:您已经在规定的时间和资金限制下提供了成功的软件。为了实现这个目标,您当时在业务压力之下不得不做出一些妥协。现在,为了以可预见的稳定速度发展,您需要处理这些妥协的不良后果。整个组织已经承受了这些债务,现在是时候清偿了。

下一个挑战是向非技术人员证明技术债务的巨大危害。根据我的经验,企业管理层相信“数字”和“事实”支持的量化数据论点。之所以为“数字”和“事实”加引号,是因为我们都清楚,我们生活在一个相对世界里,单凭任何一个数字(圈复杂度、传出耦合、代码行、测试范围等等)都不足以销售一次更改计划。除这一困难外,您还需要使用经济术语阐明消耗最大的领域:为什么这个过程比较慢;为什么此功能需要这么多花费?

事实消除疑虑

构建您的案例时,可以借用 Dale Carnegie 管理培训系统中一个非常有用的工具,该工具的精髓体现在短语“evidence defeats doubt”(事实消除疑虑)之中。作为这些管理系统(以及我们通用准则)中的一种惯用做法,消除 (DEFEATS) 部分实际上是一个首字母缩略词。我将详细讲述此工具在软件开发中的一些应用方式。请注意,我省略了第二个 E(表示 Exhibit),因为它看起来与第一个 E(表示 Example)重复。

D 表示“演示”(Demonstration)。最好的沟通方法莫过于一边展示一边讲述,而这就是“演示”的定义。如果您对开发速度进行了跟踪,那么阐述问题应该变得非常简单。显示速度随时间推移逐渐下降(见图 2),同时将此归因于灵活度日益下降且难以更改的代码。一旦您的推销有所成果,就要再接再厉。

图 2 跟踪开发速度

如果您使用如 Scrum 或极限编程这样的敏捷过程,客户反馈活动将成为一项重要实践。在开发迭代的最后,要向客户演示新功能。在您遇到技术债务陷阱以致功能的质量和数量下降时,以及当您的改进工作越来越艰难时,您应当能够演示收益随时间的推移而发生的变化。债务越少,产出越大,而产出越大,可以演示的内容就越多。

常言道:“没有实践就没有发言权。”如果有更懂技术的经理,应鼓励该经理与开发人员共同解决基本代码中的较难部分,让他/她亲身体验更改的难度。请他/她看一些代码。代码是否可以看?又是否能看懂?这是最快的说服方法了。

E 表示“示例”(Example)。具体示例所发挥的作用独一无二。搜寻一些因为技术债务而无法完成或造成明显退步的示例或要求。挑出一段不具可读性、错综复杂、充斥着副作用的代码。阐明具有如此性质的代码如何会导致客户发现缺陷或需要投入大量质量保证精力。

敏捷过程提供的另一个强大工具是追溯。选出一个在前两次迭代中出现问题的示例并询问“为什么”。探究这一特定示例何以未能完成、耗用两倍于平均示例的时间或至少跨越一个迭代。软件不够灵活往往是罪魁祸首,或者您可能因为回归 Bug 难以克服而撤消了更改。如果发现最后一个“为什么”归结为技术债务相关原因,请以简单直接的方式予以分析。这是又是您的一笔功劳,也是您论点的又一个论据。

F 表示“事实”(Fact)。事实很容易收集。您是否按时发布了项目?上次发布的缺陷率是多少?不同时间段团队的平均速度是多少?客户对交付的软件是否满意?这些都是可以摆到洽谈桌上的事实,我相信这些事实对于有经济头脑的人来说最为奏效。

在此,协作是一个关键因素。作为开发人员,您轻易就可以提供技术事实。您应向掌管经费的人员寻求帮助。对于可以说明技术债务危害性的业务事实,他们很可能有更清晰的认识,并更容易获取相关数据。

A 表示“比喻”(Analogy)。我发现这种做法尤其重要。业务人员有时会觉得软件开发混乱无序,甚至深奥神秘。如果您与项目发起人谈论耦合、聚合和单一责任原则,很有可能把他们谈跑。但这些是专业软件开发领域非常重要的概念,而且最终是您构建数据驱动案例以解决债务问题的依靠。我的建议是避免使用这些专业术语,而采用比喻解释这些概念。

例如,您可以将耦合比作纸牌搭成的房子。向项目发起人解释,速度之所以下降,是因为更改代码就好比在已经建好的非常复杂的纸牌房子上再加一堵墙、一个天花板或一层:这又好比外科手术需要非常稳定的手、一定的时间和耐心,但结果仍然不确定且另人焦急。有时纸牌房子会倒塌。

在进行比喻时,最好说明您正在使用比喻。应简要说明要传达的更具技术性的概念,以说明比喻的用意。比如,在使用纸牌房子示例时,可以说:“这就是耦合对我们更改和添加新功能的能力所造成的影响”。

T 表示“证言”(Testimonial)。有时,第三方的佐证更具效力。此第三方可以是业界领导者或顾问。第三方的证言之所以比您自己的论述更有说服力,是因为他们被视为具有客观立场的专家。

如果您没钱外聘专家,可以考虑免费收集一些业界思想领袖的趣闻佚事和真知灼见。虽然关于所谓最佳实践的一般证言不大可能起到决定性作用,但可以对您的整体论点添砖加瓦。

S 表示“统计信息”(Statistics)。数字是会说话的。有一句常用管理习语“不能度量,则无法管理”。我不确定这一传统智慧是否四海皆准,但您肯定能够举出一个例子。耦合和复杂度就是两个度量指标,可以反映吞吐量(交付的工作量)下降与因债务而逐渐僵化的基本代码之间的因果关系。

我发现这时使用复合统计最有可能奏效;如果您可以证明随着开发速度的降低,代码覆盖率指标也在逐渐降低(这因而暗示了两者之间存在联系),就能更加容易地说明代码覆盖率的重要性。

指定负责人

如果有一个得力的负责人,擅长从商业角度进行沟通并且对组织中的决策者有一定影响力,那么获得解决技术债务问题通行证的过程会顺利很多。这位负责人通常是您的主管、您的主管的主管、CTO、工程副总裁或身居类似权威职位的人。

这引出了一个有趣的鸡和蛋的问题。您如何说服此人当负责人?“向上管理”的过程也是开发人员的责任。您面临的第一个挑战是说服别人去说服其他人。具体该怎么做?事实消除疑虑!

后续步骤

至此,我已经介绍了如何组建团队找出债务问题并构建了解决债务问题的一个案例。我要重申:团队共识与客户认同是这些步骤的关键因素。

要将脚步放小,不要投入过多时间。第一次认定债务所花的时间可能比确定新改进机会的时间要长,但一旦建起管理案例,只需直接纳入计划处理的项目即可。时刻关注生产力可以节省大量精力。

在以后的文章中,我将介绍此工作流的其余部分,包括清偿债务的策略。此外,我还将介绍如何使此过程反复进行,并吸取前期债务清偿工作中的教训。

Dave Laribee 是 VersionOne 产品开发团队教练。他经常在地方和国家级开发活动中发表演讲,层获 2007 和 2008 年 Microsoft Architecture MVP 殊荣。他在 CodeBetter 博客网(网址为 thebeelog.com)发表博客文章。

衷心感谢以下技术专家对本文的审阅:Donald Belcham