上个周末,我花了两天时间,把Martin Fowler的《重构:改善既有代码的设计》重新读了一遍。

这本书我大学的时候就翻过,那时候刚学编程,看不太懂,觉得就是讲怎么改代码的,没什么大不了的。后来工作了几年,写了不少代码,也维护了不少别人的代码,踩了很多坑,再回头看这本书,感受完全不一样了。

那个周末读完之后,我坐在书桌前愣了很久,想了很多。想自己写过的那些烂代码,想维护过的那些屎山项目,想工作中遇到的各种技术债务,想软件开发的本质,想自己作为一个程序员的职业成长。

这篇文章就来聊聊《重构》这本书,以及读完之后我的一些思考和感悟。

这本书在讲什么

先简单介绍一下这本书。

《重构:改善既有代码的设计》(Refactoring: Improving the Design of Existing Code),作者是Martin Fowler,1999年首次出版,2018年出了第二版(JavaScript版)。这本书是软件开发领域的经典之作,几乎每个程序员都听说过,很多人也读过。

书的核心思想很简单:重构是在不改变软件可观察行为的前提下,改善其内部结构,使其更易理解、更易维护、更易扩展。说白了,就是在不改变功能的前提下,把代码写得更好。

书的内容大致可以分为几个部分:

第一部分是重构的原则。介绍了什么是重构,为什么要重构,什么时候重构,重构的原则和注意事项。Martin Fowler强调,重构不是重写,不是推翻重来,而是小步快跑、持续改善。重构的目的是让代码更容易理解和维护,而不是为了追求完美的设计。

第二部分是代码的坏味道。这部分是全书最经典的部分之一,Martin Fowler列举了22种代码的"坏味道"(Bad Smells),比如重复代码、过长函数、过大类、过长参数列表、发散式变化、霰弹式修改、依恋情结、数据泥团、基本类型偏执、switch语句、平行继承体系、冗余类、纯稚的数据类、被拒绝的馈赠、过度耦合的消息链、中间人、狎昵关系、异曲同工的类、不完美的库类、数据类、注释过多等。每种坏味道都有详细的描述和对应的重构手法。这部分内容非常实用,程序员可以对照着检查自己的代码,发现问题。

第三部分是重构的手法。这部分是全书的核心,详细介绍了几十种重构手法,比如提取函数、内联函数、提取变量、内联临时变量、改变函数声明、封装变量、变量改名、参数对象、保持对象完整、函数改名、属性改名、无类化、提取类、内联类、隐藏委托关系、移除中间人、引入外决方法、引入本地扩展、搬移函数、搬移字段、搬移语句到函数、搬移语句到调用者、以函数调用取代内联代码、拆分循环、以管道取代循环、移除死代码、搬移函数到对象、拆分阶段等。每种手法都有详细的步骤、示例和动机,程序员可以照着做。

第四部分是重构的实践。介绍了在实际项目中如何进行重构,比如如何在开发过程中融入重构,如何处理大型重构,如何测试重构,如何和团队协作进行重构等。这部分内容比较实用,帮助程序员把重构从理论应用到实践中。

总的来说,这本书是一本关于如何改善代码质量的实用指南,既有理论原则,又有具体手法,还有实践建议,是每个程序员都应该读的经典之作。

重构的核心思想

读完这本书,我觉得重构的核心思想可以概括为以下几点。

第一,代码是写给人看的,不是写给机器看的。这是重构最根本的出发点。很多程序员写代码的时候只关注功能能不能实现,只关注机器能不能运行,却忽略了代码的可读性和可维护性。但实际上,代码的生命周期里,大部分时间是被人读的,而不是被机器运行的。一个程序员写的代码,可能会被其他程序员读无数遍,维护无数遍。如果代码写得晦涩难懂,那维护的成本就会很高,出bug的概率也会很大。

重构的目的就是让代码更容易被人理解。通过改善代码的结构、命名、注释、组织方式,让代码更清晰、更简洁、更有逻辑,让读代码的人能够快速理解代码的意图和功能。这不仅是对自己负责,也是对团队负责,对未来负责。

第二,小步快跑,持续改善。重构不是一次性的大工程,不是要花几个月时间把整个系统重写一遍。重构应该是小步快跑、持续改善的过程。每次只改一小部分,每次改完都要测试,确保功能没有变化。这样风险小,可控,出了问题也容易回滚。

Martin Fowler强调,重构应该融入日常的开发过程中。每次添加新功能的时候,先重构一下相关的代码,让它更容易扩展,然后再添加新功能。每次修复bug的时候,先重构一下相关的代码,让bug更容易被发现和修复,然后再修bug。这样日积月累,代码质量就会慢慢提升,技术债务就会慢慢减少。

第三,重构不是目的,而是手段。重构本身不是目的,重构的目的是为了让代码更容易理解和维护,为了提高开发效率,为了减少bug。不要为了重构而重构,不要追求完美的设计而过度重构。如果代码已经很清晰、很容易维护了,就不需要再重构了。如果重构之后代码反而更复杂、更难理解了,那就是过度重构了,还不如不重构。

Martin Fowler说过:"重构是为了让代码更容易理解,而不是为了让代码更复杂。"重构的时候要时刻记住这个目的,不要陷入技术洁癖,不要为了追求所谓的"优雅"而把简单的问题复杂化。

第四,测试是重构的安全网。重构的前提是有测试。因为重构是在不改变功能的前提下改善代码结构,如果没有测试,你怎么知道改完之后功能没有变化呢?所以在重构之前,一定要有足够的测试覆盖,确保改完之后运行测试,测试通过了就说明功能没有变化。

Martin Fowler强调,测试是重构的安全网。没有测试的重构是危险的,很容易改出bug。如果代码没有测试,应该先补测试,再重构。当然,很多遗留代码没有测试,这时候可以先写一些 Characterization Test(特征测试),记录代码现有的行为,然后再重构。

这些核心思想,看起来简单,但真正理解并做到并不容易。很多程序员工作了很多年,还是没有真正理解重构的意义,还是在不断地写烂代码,不断地积累技术债务。

代码的坏味道

书里最让我有共鸣的是"代码的坏味道"这一部分。Martin Fowler列举的22种坏味道,每一种我都在实际项目中遇到过,甚至自己写过。读完之后我对照着反思了一下自己写过的代码,发现几乎每种坏味道都犯过,真是惭愧。

这里挑几种最常见的坏味道聊聊。

第一种是重复代码。这是最常见的坏味道,也是最容易被忽视的。很多程序员写代码的时候喜欢复制粘贴,一段代码在好几个地方出现,稍微改一改就用了。这样写的时候确实快,但维护的时候就麻烦了。如果这段代码有bug,需要改好几个地方,很容易漏改,导致有的地方改了有的地方没改,行为不一致。如果需要加功能,也要改好几个地方,效率很低。

重复代码的重构手法很简单,就是提取公共的函数或者类,把重复的代码放到一个地方,其他地方调用。这样改的时候只需要改一个地方,维护成本大大降低。但很多程序员就是懒得提取,觉得复制粘贴更快,结果技术债务越积越多。

第二种是过长函数。很多程序员喜欢写很长的函数,一个函数几百行甚至上千行,什么都干。这样的函数读起来非常痛苦,你需要从头读到尾才能理解它在干什么,而且很容易遗漏细节。维护的时候也很麻烦,改一个小功能可能要在几百行代码里找来找去,很容易改出bug。

Martin Fowler说过,一个函数最好不要超过一屏,超过一屏就应该考虑拆分了。过长函数的重构手法是提取函数,把一个大函数拆成几个小函数,每个小函数只做一件事,函数名清晰地描述它的功能。这样读代码的时候,看函数名就知道每个部分在干什么,需要看细节的时候再点进去看,可读性大大提高。

第三种是过大类。和过长函数类似,过大类就是一个类太大了,承担了太多的职责,有很多属性和方法。这样的类很难理解,也很难维护,因为它什么都管,改一个地方可能影响很多地方。过大类往往违反了单一职责原则,一个类应该只做一件事。

过大类的重构手法是提取类,把一个大类拆成几个小类,每个小类承担一个职责。这样每个类都比较小,职责清晰,容易理解和维护。

第四种是过长参数列表。很多函数的参数列表很长,七八个甚至十几个参数,调用的时候很容易传错,而且参数顺序也很难记。过长参数列表往往说明这个函数做了太多事情,或者这些参数之间有密切的关系,可以组织成一个对象。

过长参数列表的重构手法是参数对象,把相关的参数组织成一个对象,传递对象而不是传递一堆参数。这样参数列表就变短了,代码也更清晰。或者保持对象完整,直接传递整个对象而不是传递对象的几个属性。

第五种是霰弹式修改。霰弹式修改就是一个小的功能变更,需要修改很多个类、很多个地方,而且这些修改分散在各个地方,很容易漏改。霰弹式修改往往说明职责划分不合理,相关的逻辑分散在各个地方,没有聚合在一起。

霰弹式修改的重构手法是搬移函数和搬移字段,把相关的逻辑和数据聚合到同一个类里,这样修改的时候只需要改一个地方就够了。

第六种是依恋情结。依恋情结就是一个函数对另一个类的兴趣超过了对自己所在类的兴趣,它频繁地访问另一个类的属性和方法,而对自己所在类的东西用得很少。这说明这个函数放错了地方,应该放到它感兴趣的那个类里。

依恋情结的重构手法是搬移函数,把这个函数搬到它更感兴趣的那个类里。这样职责划分更合理,代码也更清晰。

这些坏味道只是其中的一部分,书里还有很多种。每一种坏味道都对应着具体的重构手法,程序员可以对照着检查自己的代码,发现问题并改善。这部分内容非常实用,我觉得每个程序员都应该经常翻一翻,对照着检查自己的代码。

那个周末我想了什么

读完这本书的那个周末,我想了很多,这里分享几个主要的思考。

第一个思考是:我写过多少烂代码?读完书里的22种坏味道,我开始反思自己写过的代码。从大学时候写的课程作业,到工作后写的项目代码,我发现自己写过的烂代码真不少。重复代码、过长函数、过大类、命名不清晰、注释过多、逻辑混乱……几乎每种坏味道都犯过。

印象最深的是刚工作的时候,为了快速完成需求,写了一个两千多行的函数,什么都干,从数据查询到业务逻辑到页面渲染全在一个函数里。当时觉得自己很厉害,一个函数搞定了所有事情。后来这个函数出了bug,我花了整整一天才找到问题所在,改的时候又怕影响其他功能,小心翼翼的。那时候我才意识到,写代码的时候图快,维护的时候就要还债。

还有一次,我为了赶进度,复制粘贴了另一个模块的代码,改了改就上线了。后来那个模块的代码有bug,我改了原来的地方,却忘了改复制的地方,结果线上出了问题,被领导批评了一顿。那时候我才真正理解了重复代码的危害。

反思这些经历,我觉得很多程序员(包括我自己)写烂代码,不是因为能力不够,而是因为意识不够。没有意识到代码质量的重要性,没有意识到维护成本的存在,只关注眼前的开发速度,忽略了长期的维护成本。等后来维护的时候踩了坑,才意识到当初写的代码有多烂,但已经晚了。

第二个思考是:为什么我们总是在积累技术债务?工作了几年,我发现几乎每个项目都有技术债务,几乎每个程序员都在抱怨代码烂、屎山、维护难。但为什么我们总是在积累技术债务呢?为什么就不能一开始就写好代码呢?

我觉得原因有几个。一是时间压力。项目总是有deadline,总是要赶进度,产品经理总是在催,领导总是在问什么时候能上线。在时间压力下,程序员往往会选择最快的方式实现功能,而不是最好的方式。先把功能做出来再说,代码质量以后再优化。但"以后"往往就是"永远不会",因为后面又有新的需求,新的deadline,根本没时间回头优化。技术债务就是这样一点点积累起来的。

二是意识不足。很多程序员(特别是初级程序员)没有意识到代码质量的重要性,觉得只要功能实现了就行,代码写得怎么样无所谓。他们没有接受过良好的代码质量教育,没有读过《重构》《代码整洁之道》这样的书,不知道什么是好代码,什么是坏代码。在他们看来,能跑起来的代码就是好代码。这样的意识水平,写出来的代码自然好不到哪里去。

三是缺乏规范和流程。很多团队没有代码规范,没有代码审查,没有测试要求,程序员写代码全凭个人习惯。有的人写得好,有的人写得烂,没有统一的标准。没有代码审查,烂代码就能直接合入主干,没有人发现和纠正。没有测试,重构就没有安全网,程序员不敢重构,只能在烂代码的基础上继续堆烂代码。缺乏规范和流程的团队,技术债务必然会越积越多。

四是人员流动。IT行业人员流动很大,一个项目可能换了好几拨人。每个人的编码习惯不一样,每个人对业务的理解不一样,每个人写代码的水平也不一样。前面的人写了烂代码走了,后面的人不了解上下文,不敢大改,只能在原来的基础上修修补补,结果代码越来越烂。人员流动加速了技术债务的积累。

这些原因加在一起,导致几乎每个项目都有技术债务,几乎每个程序员都在和烂代码作斗争。但技术债务不是不可避免的,只要团队有意识、有规范、有流程,是可以控制技术债务的。关键是要重视,要行动,而不是一边抱怨一边继续写烂代码。

第三个思考是:重构在实际工作中真的可行吗?读完《重构》,我相信重构的理念是对的,但在实际工作中,重构真的可行吗?我自己的经验是,说起来容易做起来难。

最大的困难是时间。项目总是有做不完的需求,产品经理总是在催进度,领导总是在问什么时候能上线。你说要重构,产品经理说这个需求很急,先做需求,重构以后再说。领导说重构能带来什么业务价值?能提升KPI吗?不能的话先放放。在这样的环境下,重构很难排上优先级,往往只能在业余时间偷偷做,或者在需求开发的间隙顺便做一点。

第二个困难是风险。重构是有风险的,特别是对于没有测试的遗留代码。你改了代码,怎么保证功能没有变化?怎么保证没有引入新的bug?如果出了问题,谁来负责?很多程序员因为怕担风险,不敢重构,特别是对于核心模块的代码,更是不敢碰,怕改出问题影响线上。结果就是烂代码一直烂着,没人敢动,只能在外面包一层又一层。

第三个困难是团队协作。重构不是一个人的事情,特别是大型重构,需要整个团队的配合。如果团队里有的人认同重构,有的人不认同,有的人写代码质量高,有的人写代码质量低,那重构就很难推进。你辛辛苦苦重构了代码,别人又写了烂代码进去,你的努力就白费了。团队需要有统一的代码规范和代码审查流程,才能保证重构的成果不被破坏。

第四个困难是度量。重构的效果很难度量。你重构了代码,代码质量提升了,但怎么证明呢?代码行数减少了?圈复杂度降低了?bug减少了?开发效率提升了?这些指标都很难直接和重构挂钩。领导看不到直接的业务价值,就不会支持重构。程序员做了重构,也很难在绩效上体现出来,自然就没有动力。

这些困难都是实际存在的,导致重构在很多团队里推行不下去。但我觉得,不能因为有困难就放弃重构。可以从小处着手,在日常开发中融入重构,每次改代码的时候顺便重构一下周围的代码,日积月累,代码质量就会慢慢提升。不需要一开始就搞大型重构,不需要专门申请时间,把重构变成一种习惯,变成日常开发的一部分,就可行了。

第四个思考是:程序员的职业成长和代码质量的关系。读完这本书,我还想到了程序员的职业成长。我发现,代码质量和程序员的职业成长有密切的关系。

初级程序员往往不重视代码质量,觉得只要功能实现了就行。他们写的代码往往有很多坏味道,重复代码、过长函数、命名不清晰等。这很正常,因为他们还在学习阶段,主要关注的是怎么实现功能,还没有精力关注代码质量。

中级程序员开始意识到代码质量的重要性,开始学习重构、设计模式、代码规范等知识,开始有意识地写高质量的代码。他们会主动重构自己的代码,会在代码审查中提出改进意见。这个阶段的程序员,代码质量有了明显的提升,也开始能够维护和改善别人的代码。

高级程序员不仅自己写高质量的代码,还能够推动整个团队的代码质量提升。他们会制定代码规范,推动代码审查,建立测试体系,组织技术分享,培养团队的代码质量意识。他们能够处理大型重构,能够处理复杂的技术债务,能够在技术债务和业务需求之间找到平衡。这个阶段的程序员,已经不仅仅是写代码了,而是在提升整个团队的技术水平。

我觉得,重视代码质量、学会重构,是程序员从初级到中级的一个重要标志。而能够推动团队代码质量提升、处理大型技术债务,是从中级到高级的一个重要标志。代码质量不仅仅是代码的事情,更是程序员职业能力和职业素养的体现。

回顾自己的成长历程,我也是从写烂代码开始的,到后来慢慢意识到代码质量的重要性,开始学习重构,开始有意识地写高质量的代码。现在我还在这个过程中,还有很多需要学习和提升的地方。但至少我已经意识到了,已经在行动了,这就够了。

给程序员朋友的建议

最后给程序员朋友一些建议,关于代码质量和重构的。

第一,认真读一遍《重构》。这本书是经典中的经典,每个程序员都应该认真读一遍。不要像我大学时候那样翻一翻就放下了,工作几年之后再读,会有完全不一样的感受。书里的22种坏味道和几十种重构手法,非常实用,可以对照着检查和改善自己的代码。

第二,把重构变成日常习惯。不要等到代码烂得不行了才想起来重构,不要专门申请时间搞大型重构。把重构变成日常开发的习惯,每次添加新功能的时候先重构一下相关代码,每次修复bug的时候顺便改善一下周围的代码,每次代码审查的时候提出改进意见。日积月累,代码质量就会慢慢提升。

第三,写测试。测试是重构的安全网,没有测试就不敢重构。从现在开始,养成写测试的习惯。新写的代码一定要有测试覆盖,维护旧代码的时候先补测试再重构。有了测试,重构就有了底气,就敢改代码了。

第四,不要追求完美。重构不是为了追求完美的设计,而是为了让代码更容易理解和维护。不要过度重构,不要为了所谓的"优雅"而把简单的问题复杂化。够用就好,适度就好。Martin Fowler说过:"让代码工作,然后让代码正确,最后让代码快速。"重构也是一样,先让代码能工作,然后再慢慢改善。

第五,和团队一起进步。代码质量不是一个人的事情,需要整个团队的共同努力。和团队一起制定代码规范,一起做代码审查,一起学习重构和设计模式,一起提升代码质量意识。一个人的力量是有限的,团队的力量是无穷的。只有整个团队的代码质量都提升了,项目的代码质量才能真正提升。

第六,不要怕犯错。重构的时候可能会犯错,可能会引入bug,这很正常。不要因为怕犯错就不敢重构。只要有测试覆盖,只要小步快跑,只要及时回滚,犯错的风险是可控的。从错误中学习,不断提升自己的重构能力,这才是最重要的。

这些建议都是我自己的经验和感悟,希望能对程序员朋友们有所帮助。

写在最后

读《重构》的那个周末,我想了很多。

我想到了自己写过的那些烂代码,想到了维护过的那些屎山项目,想到了工作中遇到的各种技术债务,想到了软件开发的本质,想到了自己作为一个程序员的职业成长。

《重构》这本书,表面上是讲怎么改代码的,但实际上讲的是一种态度,一种对代码质量的态度,一种对职业的态度。它告诉我们,代码不仅仅是实现功能的工具,更是给人看的,是需要维护的,是有质量高低之分的。它告诉我们,程序员不仅仅是写代码实现功能,更要对代码的质量负责,对项目的长期维护负责,对团队和用户负责。

在这个快速迭代、追求速度的时代,很多人忽略了代码质量,很多项目变成了屎山,很多程序员在烂代码中挣扎。但我相信,随着行业的成熟,随着程序员职业素养的提升,代码质量会越来越受到重视,重构会越来越成为日常开发的一部分。

作为一个程序员,我能做的就是从自己做起,从现在做起,写好每一行代码,做好每一次重构,不断提升自己的代码质量和职业素养。也许一个人的力量有限,但只要每个人都这样做,整个行业的代码质量就会慢慢提升。

最后用Martin Fowler的一句话来结尾:"任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。"

愿我们都能成为写出人类容易理解的代码的优秀程序员。