如果你正在对经理说你要让整个团队花上两周来重构代码。经理问:“好的。你会给我什么样的新功能呢?” 我说:“重构修改内部结构而不改变外部行为。不会有任何新功能。” 他看着我问道:“那你为什么要重构?” 我应该如何回答?

活动预告:12月22~23,GIAC全球互联网架构大会将于上海举行,本周新增阿里,百度、今日头条等多名讲师出席,参看文末了解更多详情。

重构是指在不改变外部行为的前提下对代码的内部结构进行重组或重新包装。

想象一下,如果你是若干年前的我,正在对经理说你要让整个团队花上两周(一个完整的迭代周期)来重构代码。经理问:“好的。你会给我什么样的新功能呢?”

我说:“等等。我是说重构。重构修改内部结构而不改变外部行为。不会有任何新功能。”

他看着我问道:“那你为什么要重构?”

我应该如何回答?

软件开发者时常遇到这样的情况。有时候不知如何作答,是因为我们和管理层存在沟通障碍。我们使用的是开发者的语言。

我不能告诉经理重构代码是为了好玩,是因为它让我感觉良好,或者因为我想要学习Clojure或者其他新技术……这些对管理者来说都是不可接受的答案。我必须强调重构对于公司的重要意义,而且它确实意义重大。

开发者知道这些,但需要用恰当的词汇也就是商务用语来表达,其实就是收益风险

我们如何在降低风险的同时提高收益?

软件本身的特点决定了其高风险和多变性。重构能降低以下四个方面的成本:

  • 日后对代码的理解
  • 添加单元测试
  • 容纳新功能
  • 日后的重构

很显然,如果需要添加新功能或修复bug,就应该重构代码,这很容易理解。如果你以后不再碰代码,也许就不需要重构了。

重构是学习新系统运作机理的有效方式。通过对见名知意的方法进行封装或重新命名来学习代码。通过重构,我们可以补充之前缺失的实体,改善之前编写的糟糕代码。

我们都希望看到进度并如期交付,所以有时会做出妥协。重构代码能清理之前造成的障碍,为的是最终能够有所成果。

1 投资还是借贷

我在2009年4月的博客中首次讲述了以下故事。1

1Bernstein, David Scott. To Be Agile (blog). “Is Your Legacy Code Rotting?” http://tobeagile.com/2009/04/27/is-your-legacy-code-rotting

有些经营者认为开发软件是种一蹴而就的活动。如果软件在编写完成后不会变更的话确实如此,但我们身处的世界在变化,不变的软件很快就过时了。

代码的衰变是真实存在的,即使一开始编写良好的软件也常常难以预计将来会面临的变更。这实际上是好事!不需要变更的软件通常是没人使用的软件。我们希望自己构建的软件为人所用,为了软件能持续给人带来价值,它需要容易修改。

我们可以用纸板建造一间漂亮的房子,在晴朗的夏日它能支撑得住,但第一场暴雨就能摧毁它。建筑工人有一系列严格的标准和实践用来保证建筑的稳固。软件开发者也应该这样做。

我在东海岸的一位客户是全球最顶尖的财务公司之一,他们饱受大量遗留代码的摧残。大部分的遗留代码都是由合同工开发的,有些虽然由他们中的顶尖开发者开发,但是在第一版本完成后,这些开发者立刻加入了其他项目。一些初级开发者被指派来维护这些系统,有些人并不理解最初的设计,所以对系统强加修改。最后弄得一团糟。

在一次他们的高级经理及高级开发者参加的会议上,我说:“我这样说对不对?你们之所以成为顶尖的财务公司,是因为你们找到一流的基金经理管理你们的基金,权衡最佳投资,然后冻结那些资产,将这些基金经理撤出去做其他项目。”

他们说我前半句说得没错,但是基金经理一直管理着基金,持续对资产做出调整,因为市场瞬息万变。

“哦,”我说,“所以你们雇用一流的软件开发者,让他们进行设计,开发系统,在他们完成后就换到其他项目中。”

“你的意思是我们的软件也是一种关键资产,和其他资产一样需要维护?”一个经理问道。

答对了。

你会买一辆9万美元的奔驰车,然后因为嫌花费太多而不将其送去保养吗?不会。无论车有多昂贵,制造工艺有多精良,都是需要维护的。没有人会在盖房子的时候想着永远也不会更换地毯,添置新厨房器具或重新粉刷。反之,也没有人会在开车上路三天后就换变速器,理由是早晚也得换?如果有天发现倒车档失灵,你会在这种无法倒车的情况下开多久再去检修变速器呢?

有些事情最好放到最后处理,有些则不能推后。知道这两者之间的差别绝对重要。

对于那些会不断累积的技术债,尽快偿清几乎总是(肯定会有例外)正确的选择。如果任由技术债在系统中堆积,而开发者又在系统中工作,那么绝对会发生冲突。开发者会碰到那些技术债并且一次次付出代价。他们没法倒车,所以必须调整他们的行为(驾驶习惯),绕弯路到达目的地,为的是不使用倒车。一个问题会导致更多的问题。越早处理技术债,花费的成本就越低,就像信用卡欠款一样。

2 变成“铁公鸡”

技术债和财务债一样:利息会把你拖垮。

我曾经和财务信贷公司合作过,他们有一个不愿意示人的词语用来形容我这类人。我是那种总是在收到账单时就全额还款的人,从来不让我的账上产生利息。信用卡公司称我这样的人是“铁公鸡”,因为无法从我们这样的人身上挣到钱而讨厌我们。他们喜欢那些让债务堆积每次只偿还最小还款额的人。我认识的一个人欠了一家信用卡公司1.7万美元的债。如果他每个月只还最小还款额的话,需要花93年共计18.4万美元才能还清。

和财务债一样,无视问题并不能让问题消失。我希望你成为技术债的“铁公鸡”。

有时候,我们必须让技术债多存活一阵子,要不就是现在不是修复的最佳时机,要不就是我们不知道如何下手,或者单纯的没有足够时间而已。我们会经常无可奈何地发现自己处于这番境地。但是,先支付几个月的最小还款额来度过难关,然后再连本带利一起还清,和装作相安无事直到二十二世纪,这有很大差别。

我们并非试图创建完美的代码。我始终在强调这一点。没人可以做到完美无缺,软件开发者也不是在追求完美无缺。我们必须时刻清楚地权衡利弊。我是否时不时在代码中引入了问题?是的,无可避免。如果不这样,我就会丢掉饭碗。

3 当代码需要修改时

即使是写得最糟糕的遗留代码,如果我们不碰它的话也能持续产生价值——只要不进行修改。

这种判断应该分不同情况讨论。任务关键型软件的需求和视频游戏完全不同。软件之于实物机械的一个好处就是,信息不会磨损。但是,遗留代码需要修改和扩展的时候会怎样呢?

如果软件真的被使用了,人们就会发现更好的使用方式,然后提出修改需求。如果想让用户从你创建的软件中获得更多价值,就需要找到安全的方式来改进代码,以支持变化的需求。

既然我们已经知道优质代码的一些特性,就可以用重构这个工具来安全地、渐进地将代码变得更容易维护和扩展。对设计糟糕的代码进行安全地重构,让我们可以注入模拟对象来使软件可测试,这样就可以在代码中添加单元测试。单元测试这张防护网可以支持我们为了安全地添加新功能而进行更复杂的重构。

这样清理遗留代码可以在遗留代码之上工作而又不用担心引发新bug:进行渐进式修改,添加测试,然后添加新功能。如果有良好的单元测试来覆盖代码,就可以在绿条之上进行新的开发和重构了。这是更安全也更廉价的修改软件的方式。

在软件行业中,有许多的代码——遗留代码——并未按照我们预期的那样运作良好,完全没法维护,更不用说扩展了。但是我们能做些什么?又应该做些什么呢?

多数的情况是,什么也做不了。

在软件行业,我们不应该将遗留代码视为定时炸弹,而是地雷。如果代码正常工作,不需要修改或升级,就不要动它。这适用于绝大多数遗留代码。正如有句谚语所说:“东西没坏,就别去修。”

如果我们乱碰那些遗留代码,一定会出问题。如果代码如预期的那样运行,就这样使用。这对于大多数现存的软件来说都是正确的。一般来说,只重构需要修改的代码。

如果你想修复代码中的bug,或者添加新功能,或者修改现有功能,对已有代码进行修改就非常有必要。修改代码的风险和成本都很高,所以要谨慎行事。但是,如果真的需要修改代码,就应该使用正确的方法,好让修改变得安全。事实上,这些修改代码的技巧和我们之前讨论的编写优质代码的技巧是一样的。

我们可以用重构新代码的方式重构已有代码。

3.1 对已有代码添加测试

测试先行能让代码更容易测试,后期添加测试比测试先行更有挑战,却也能从整体上提高代码的可维护性,降低修改代码的成本。

变更请求是好事,意味着有人关心并希望代码得到改善。

得到变更请求之后,我们希望能够做出响应,在已有的软件中提供新功能,让客户能够在使用过程中得到更多价值。当然,还有许许多多的代码已经无人问津。那些代码可以安静地被淘汰,但是,那些正在使用着的比特——客户所倚仗的很可能会变化的那些比特——是我们重构的目标。

3.2 通过重构糟糕代码来培养良好习惯

重构是一个未被所有开发者都理解的技巧,重构也是培养良好开发习惯、展示构建可维护代码方法的工具。这些技巧自始至终是软件工程师应必备的技巧。

重构遗留代码听上去很无聊,但事实上充满惊奇和挑战。不断练习会让人得心应手。精通重构之后,有趣的事情就会发生:我们不再会写出糟糕的代码或遵循有缺陷的开发实践,而开始前构(参见《前构》[Pug05]),也就是说可以一开始就写出优质的代码。学习如何避免软件开发中的错误,如何正确进行开发,重构是我所知的最快速的方式之一。

3.3 推迟那些不可避免的

身为软件开发者,我们的目标是通过构建有价值的软件来创造价值。这意味着我们开发出来的软件需要能立刻产生价值,并在以后的日子里持续产生价值。

为了让软件在将来持续产生价值,必须降低软件所有者的开销,这样对软件进行改进和扩展才会有收益。让软件健壮且可维护是我们的首要目标,这样会降低软件所有者的开销。

但无论早晚,软件都会被淘汰。有些我编写的软件存活的时间让我大吃一惊。我在孩童时代编写的并不引以为傲的代码却以某种形式存活至今。

软件有时会夭折,有时也会比我们预期的存活得更长久,我们在编写软件的时候完全没法准确预估将来到底会怎样。但是我们都希望自己的软件能够持续产生价值。应该尽我们所能,提高对投资的回报,降低软件所有者的开销。

4 重构技巧

在我们清理代码让其更容易测试的时候,也让它变得更容易扩展且降低日后修改的开销。以下是一些重构代码的技巧。

一般来说,重构遗留代码从功能层次开始,因此可以根据一些可观测的行为编写图钉测试。

4.1 图钉测试

图钉测试是非常粗粒度的测试,它测试的可能是成百上千行代码的单一行为。虽然最终期望的是许多更细粒度的测试,但是,通过图钉测试来覆盖整体行为会让我们有一个落脚点。每次修改代码后,都可以回到图钉测试来验证最终点对点的行为是否依然正确。

由于图钉测试粒度很粗,因此必须频繁执行来验证修改是否对代码造成影响。这可以给一些相对安全的重构行为提供防护网,让代码中有更多的间隙可以进行依赖注入。这将有效解耦对象和它们所使用的服务,让我们可以用模拟对象替代服务,以便独立测试指定代码。这让更小单元的行为变得可测试,可以添加更细粒度的单元测试,用来支持更复杂的重构。

这就是重构遗留代码的方式,一点一点,进行小规模增量优化。遗留代码的产生是因为一直以来开发者所做的小修改降低了代码质量。修复的方式也是用小规模的代码修改来增进代码质量,然后逐渐减轻遗留代码的负担。

4.2 依赖注入

我们在之前讨论过分离对象的创建和使用的意义。这是让代码变得可测试的重要环节,同样也让代码变得可以独立部署。分离了对象的构建和使用,我们就可以在不引入耦合的情况下注入所需的依赖。这是面向对象编程的基本技巧之一。

各种框架都会使用这种技术,像Pivotal Software的开源Spring和Red Hat的Hibernate,都被称为依赖注入框架2。原理很简单:我们不直接创建要使用的对象,而是让框架替我们将对象注入到代码中。342准确地说,Spring是依赖注入框架,而Hibernate是使用了依赖注入的ORM框架。——译者注

3Spring. http://spring.io

4Hibernate. http://hibernate.org

用依赖注入取代创建可以解耦对象和它们所使用的服务,这会让软件更容易测试和扩展。如果不注入真实的依赖而是注入模拟依赖,就很容易测试代码。依赖注入也会使代码更容易维护,它有助于将业务决策集中化,简化对象使用方式。通常,为了理解一个新系统,首先看的就是对象实例化的地方。我们查看工厂对象,依赖注入框架,或者其他进行对象实例化的地方。这会告诉我们很多关于系统的信息,让我们知道如何改进它。

4.3 系统扼杀

如果需要在不影响系统的前提下替换一个组件,通常使用Martin Fowler在2004年最早提出的系统扼杀5。先用一个自己的服务将原来的服务包裹起来,然后一点点替换原来的服务,直到原来的服务最终被扼杀。

5Fowler, Martin. Martin Fowler (blog).“Strangler Application.”June 2004. http://www.martinfowler.com/bliki/StranglerApplication.html

为新的服务创建一个新的接口来取代老的服务。然后客户端代码使用新的接口,即使新的接口仅仅是指向老的服务。这样可以阻止老的服务继续扩散使用,让新的客户端使用新的接口,新的接口背后的代码最终会被替换为新的整洁的代码。

这样就可以不慌不忙地重构已有的系统,直到老的接口仅仅是薄薄一层外壳,它只是对重构好的新代码进行调用而已。可以选择继续支持遗留的客户端,或者让它们也进行重构,用全新的接口彻底淘汰遗留系统。系统扼杀是非常有效的重构遗留代码的方法,它可以在重构的同时保持系统持续可用。

4.4 抽象分支

这里要介绍的最后一个技术也是Martin Fowler提出的,它叫抽象分支6。先不管名字,这是一个版本管理技巧,帮助我们消除分支。原理是针对想要修改的代码提取出一个接口,然后编写新的实现,但是老的实现依然参与构建,在构建期间用功能开关隐藏正在开发的功能不让用户感知到。

6Fowler, Martin. Martin Fowler (blog).“Branch by Abstraction.”January 2014. http://martinfowler.com/bliki/BranchByAbstraction.html

一切就绪之后,可以打开功能开关用新的接口替代老的接口。这是简单且直观的方法,却消除了软件的版本分支依赖。版本分支依赖对于很多软件开发团队来说是个大问题。正如之前所说,一旦开始用功能分支,我们就不再是进行迭代开发,而是沦为了瀑布式开发。

有时系统耦合之紧密,无法在不影响其他地方的情况下打破依赖。这时Ola Ellnestam和Daniel Brolund的《天皇法则》(The Mikado Method)[BE12]能帮助我们解决这种纠缠不清的依赖。

5 以支持修改为目的重构

当然,还有许多其他应对遗留代码的技巧。基本原理都是:清理代码,让代码容易维护、容易理解,然后添加测试以便安全地进行修改。最后,必须在有单元测试的保障之后,对代码进行大规模重构。

我们把重构当作一门学问,而且还有很广泛的研究空间。我们需要继续研究这种规范化的代码修改方式。我更愿意有一套安全且可复用的修改代码方法论来分享给其他开发者,而不是凭着直觉修改代码(这种直觉无法直接告诉其他人),尽管有时候这样做效率更高。

重构软件的目的就是可以容易地根据客户的希望修改软件。这无法通过阅读客户的思想或预知未来来达成,而是遵循着一些健壮性的原则和实践,让代码在需要的时候可以被修改。这需要有一套单元测试、准确的领域建模、合理的抽象、CLEAN的代码以及其他优秀的技术实践。当我们做到这些的时候,会让修改代码变得无痛,可以随时修改代码,更好地迎合客户的需求。

6 以开闭原则为目的重构

这是改变我一生的几个字,它帮助我摆脱了遗留代码的窠臼。重构是在不改变外部行为的前提下调整设计。开闭原则是指软件实体应该“对扩展开放而对修改关闭”。换句话说,力求在添加新功能的时候做到添加新代码并将现有代码的修改最小化。避免修改现有代码是因为很可能会引发新的bug。

以开闭为目标重构是安全高效添加新功能的方式。让每一个改动都分为两步。第一步重构想要扩展的代码,让它可以容纳新功能。这并非添加功能,而是在原有的软件中通过添加抽象或定义接口之类的方式来给新功能创建空间。在有单元测试的前提下重构代码来容纳新功能,这样做是安全的、没有阻碍的,如果你犯了错单元测试立马就会告诉你。这是安全且代价小的修改代码的方式。

当代码重构完成可以容纳新功能的时候,第一步重构阶段就完成了。接下来的一步是增强阶段。先编写失败测试描述要实现的新功能,然后添加功能让测试通过,以开放的增量方式开发。我们只添加代码,因为之前的重构阶段已经完成了修改代码。我们可以在不修改过多代码的前提下添加代码,这样更安全。最后,重构新添加的代码,让它容易理解和维护。

如果尝试一次将所有的事情都做完,正如很多开发者做的那样,那很容易迷失其中然后犯错误,结果为之付出巨大代价。但是,在有单元测试覆盖的前提下分阶段做,会让修改软件变得更简单、风险更小。

我总是将修改代码分为两个步骤,这样做是因为我通过TDD来构建功能。需要在现有系统中添加功能的时候我也这样做。前面讨论的许多实践不仅对编写新代码适用,对遗留代码也同样适用。一旦理解了优质代码是什么样的,就更容易认识到重构遗留代码的时候应该追求什么。

7 以提高可修改性为目的重构

代码的可修改性并不是意外产生的。必须有意在新代码中创建,或者小心地在重构遗留代码过程中通过遵循优秀的开发原则和实践引入。

这对于任何专业来说都是一样的。医生不可能挥挥手就神奇地治愈了病人。尽管有时候患者会自愈,但软件无法自我编写。你必须让计算机执行你的命令。

代码对可修改性的支持意味着找到合理的抽象且代码封装良好。归根结底,可修改性来自于理解所建模事物并将这些理解连带着各种特性都灌输到模型中去,让模型准确、一致。

这些实践不会替我们做设计。TDD对设计有帮助,但我们不能停止思考让TDD替我们编码。TDD是一个工具,可以帮助我们理解构建易修改代码的流程,这个流程尤为重要。

科学与艺术的一个区别是,科学常常有一定的流程(一个进程或者一个程序)。这就像根据菜谱做饭,我们可以从同样的菜谱开始,做一些不同的修改,然后做出风味各异的同一道菜肴。我们始终遵循同样的实践:如何煎炸,如何切菜然后烹饪口感更好,如何避免做出半生半熟的鸡肉,等等。

流程对软件开发来说至关重要。虽然我们面对的每个问题都各不相同,需要不同的方法来解决,但是解决软件问题的基本通用流程还是有的,有些甚至是反直觉的,就如同测试先行和最后进行设计。

8 第二次做好

TDD用真实的用例来定义行为。用真实的用例来驱动开发比抽象的思考要容易。它有助于构建更稳定的接口,当我们有两三个用例之后,将代码通用化会比只有一个用例来得简单得多。我喜欢这样的说法:

第二次做好。

在听到我这样说之后,开发者有时候会用奇怪的眼神看我,好像我疯了一样。我们一直被教导着凡事要一次做好。

但是,当我们一次做好(或者说认为自己一次做好)的时候,我们给自己添加了很多额外的工作。只有一个用例很难进行通用化设计。在只有一个用例的时候进行具体化的编写,在有了两三个用例之后进行通用化就相对容易了。我们可以观察各个用例之间的异同,然后进行总结,找到合理的抽象。

在天文导航中,三角定位法是一个非常常用的手段。通过多个水平线上的点或者多颗星星得到的定位,比只通过一个点得到的定位要准确得多。编写代码也是同样的道理。

如果有一个非常复杂的算法,不确定如何才能完美解决,先创建几个用例。通常在两个(最多三个)用例的辅助下,就能推导出真正的算法了。这比只通过一个用例来猜想要容易得多。

有了两个用例,就可以开始对每一步进行总结,得出正确的抽象。所以,如果只有一个用例,就直接把它编写出来,当然,是使用测试先行开发。这让我们可以用真实的单元测试来驱动行为的开发,而且在得到第二个用例要重构代码的时候也提供了安全保障。

软件是软的,利用这个特性我们可以更轻松地构建出更优质、更灵活的代码。这是重中之重。

相反,试图一蹴而就会有很大的压力。对于所有人来说都一样。知道可以回过头去修改、随时清理(可以在任何时候重构),会让我们很自由。

研究重构是学习如何从一种设计转变到另一种设计的最佳方式。这种转变大都很容易,理解了如何转变设计,也就意味着可以不用试图在一开始就找到最佳设计,我们可以随着重构来不断改进设计。

对于习惯了确定性的我们,这样的策略乍看之下有些奇怪。它认定我们是处在一个万事万物都在变化的难以预计的世界之中。在习惯了这种开发方式之后,可以更快速地构建更优质的代码,而不需要提前将所有的需求都准备完毕才开始构建。这让人很自由,也是因为如此,我发现它能让开发者产生共鸣。

9 让我们付诸实践

以下是把这些想法付诸实践的方式。

9.1 助你正确重构代码的7个策略

重构给予开发者改进设计的机会,也让管理层以廉价且低风险的方式在已有系统中添加功能。以下是帮助你正确重构代码的7个策略。

从已有系统中学习

重构是一种学习代码的方式,也是将学到的东西融入代码的方式。比如,将命名不合理的方法用更合理的名字取代或包裹起来,可以提高代码的可读性。同时,我们也学习到了系统是如何工作的,并且将这些理解通过提供更合理的命名融入到了新代码中。

循序渐进

低风险重构是重构方法中的子集,可以相对简单地执行。大多数可以通过开发工具进行自动化,比如在一个工程内重命名、提取、移动方法和类。我在编码的时候经常使用这些重构方法,让代码时刻反映我对所构建系统的最新理解。

在遗留代码中添加测试

所有的重构都会降低四件事情的开销:日后理解代码、添加测试、添加新功能、更多的重构。在重构的过程中会发现改进设计、添加单元测试的机会。添加更高质量的测试之后,会更有信心进行更激进的重构,进而给编写更多高质量测试创造机会,如此往复。

始终进行重构

重构是需要自始至终进行的。编程通常都是一个发现过程。也许在探索的过程中并不知道最好的方法,所以在有了更好的理解之后才有更多的机会去改进代码、更新命名等。这是保持代码容易使用的关键。如果实践TDD,就会知道在TDD过程中,重构是在编写出可用实现之后立刻进行的。这样可以提高代码的健壮性,减少维护成本,提高可扩展性。

有更好的理解后对一个实现进行重新设计

即使持续重构,也依然会在开发过程中产生技术债。当得到会影响设计的新信息或者需要实现一个当前设计不支持的功能时,可能是时候做一次大规模的重构了。也许需要大范围的重新设计和实现,让以后更容易添加新功能。

继续其他工作前进行清理

一旦完成了某些工作,应在继续其他工作前重构现有代码,让其更加健壮。在知道了每个方法的职责之后,保证它们有合理的名称来表达其意图。保证代码容易阅读、分布合理。将大方法拆分为小方法,必要的时候提取出额外的类。

重构以避免误入歧途

现如今多数生产环境中的软件都积累了大量的技术债,迫切需要重构。这看上去像是一个可怕的任务,也确实可能会如此,但是重构代码也可以非常有趣。我发现自己在重构代码时收获颇丰,在花费大量时间清理他人(或自己)的错误后,在编写新代码的时候能避免类似的错误。随着重构越来越多,我也随之成为更优秀的开发者。

重构是为了降低风险减少浪费。高效的开发团队可能花费一半的时间在重构代码上,同时也优化了他们的设计,提高了系统的健壮性,时间花得很值。因为代码被阅读的次数是编写次数的十倍以上,利用重构来清理代码会很快得到收益。

9.2 决定何时进行重构的7个策略

鉴于整个行业中需要重构的代码远远多于我们的承受能力,我们需要决定对哪些代码进行重构。如果生产环境上的软件正常工作不需要扩展,则无需重构代码。重构代码有风险和成本,所以我们希望最后收益能够抵得上开销。以下是决定何时进行重构的7个策略。

当关键代码维护不善的时候

多数软件的状况无法进行安全的重构。如果代码处在生产环境,对它进行修改,即使是看上去很小的改动,也会造成未知的破坏。因此,别去碰触遗留代码总是明智的。但是当关键代码难以理解变成累赘时,就是时候进行清理了。这种场景下,添加测试来支持更复杂的重构非常有效。

当唯一理解代码的人没空的时候

我们编写的软件应该可以让团队的其他成员容易理解和维护,但是,有时现有的代码只有那些特定的“专家”才能维护。这对公司来说不是一件好事。如果代码需要维护更新,让关键人员在继续其他工作前花时间清理代码,这可以避免后期花费更大的成本进行清理。

当有信息可以揭示更好的设计的时候

需求,以及我们对于需求的理解,都是一直在变化的。当有了更好的设计方案,而且收益比成本要高的时候,重构就是个好主意。这是一个持续进行的过程,用来保证软件整洁且与时俱进。通过一系列的重构来改进设计,是非常有效的保持软件可维护的方法。

当修复bug的时候

有些bug仅仅是拼写错误而已,而有些则代表了设计上的缺陷。很多时候,代码的bug体现了开发流程上的缺陷,或者至少是系统中一个缺失的测试。也许是因为难以编写测试所以缺失,这样的话,我们可以重构代码,让编写测试变得容易,然后补全测试。接着再修复bug,测试通过之后则一切正常。

当需要添加新功能的时候

向不兼容新功能的系统中添加新功能的最廉价、最安全的方式就是,先重构代码让系统可以兼容新功能,重构完毕之后再添加新功能。我们不会希望自己同一时间对多处代码进行修改。为添加新功能而重构代码,通常需要添加新的抽象和接口,让新功能更容易插入到现有系统中。重构之后,向代码中添加新功能就应该轻而易举了。

当需要为遗留代码写文档的时候

有些代码很难理解,在编写文档前通过简单的重构和清理就能有很大帮助。编写文档的目的就是为提高系统的保障性,这也是重构的目的之一。

当重构比重写容易的时候

将生产环境上的系统直接抛弃彻底重写,几乎从来都不是个好主意。重写一个应用通常都会比原来的系统积累更多技术债。如果重写得不够彻底,很可能也会犯之前一样的错误。重构则是一个安全的逐步清理代码的系统性方式,同时也可以保持系统持续运作。

重构有开销,而且有许多代码需要重构。为了在有限的资源下做出最好的选择,必须有针对性地重构。当需要改动代码(比如修复bug或者添加功能)的时候,通常都是重构的好时机。把握这些重构的机会,就会让代码更容易维护和使用。

10  总结

在代码需要修改的时候重构遗留代码。使用重构技巧有条理地进行修改。我们的思想应该由外部的质量监控转移到通过重构来提升代码可维护性并降低软件所有者的开销上。

本文中心思想如下。

  • 学习如何有效地清理代码,以偿还技术债。
  • 将为新功能创建容纳空间和开发新功能分开,可以大大简化任务,降低引入bug的风险。
  • 更有效地清理代码,理解为什么在构建软件时要持续改进设计。
  • 熟悉重构之后,自然会编写出更整洁的代码。

重构,以及如何正确重构,是清理遗留代码的重要手段。重构是修改或更新遗留代码的第一步,同时,也要对新编写的代码进行重构,防止它演变成遗留代码。重构也是学习现有代码库的有效方法。

本文节选自《修改软件的艺术:构建易维护代码的9条最佳实践》第13章,由图灵教育(turingbooks)授权高可用架构发表。如需了解本书更多内容,可在各大网店购买。

活动预告

12月22~23日,GIAC 全球互联网架构大会将于上海举行。GIAC 全球互联网架构大会是高可用架构技术社区推广的面向架构师、技术负责人及高端技术从业人员的技术架构大会。GIAC于2016年12月成功举办了第一届,而今年的 GIAC 已经有腾讯、阿里巴巴、百度、平安、饿了么、携程、七牛、蚂蚁金服、罗辑思维、摩拜、唯品会,LinkedIn, Pivotal, Mesosphere, AdMaster, Hulu 等公司专家出席。

参加 GIAC,盘点年底最新技术,目前单人购买优惠 600 元,多人购买有更多优惠,点击阅读原文了解大会详细议程。

640_wx_fmt_png