理论一:什么情况下要重构?到底重构什么?又该如何重构?
# 27 | 理论一:什么情况下要重构?到底重构什么?又该如何重构?
“重构”这个词对于大部分工程师来说都不陌生。不过,据我了解,大部分人都只是“听得多做得少”,真正进行过代码重构的人不多,而把持续重构作为开发的一部分的人,就更是少之又少了。
一方面,重构代码对一个工程师能力的要求,要比单纯写代码高得多。重构需要你能洞察出代码存在的坏味道或者设计上的不足,并且能合理、熟练地利用设计思想、原则、模式、编程规范等理论知识解决这些问题。
另一方面,很多工程师对为什么要重构、到底重构什么、什么时候重构、又该如何重构等相关问题理解不深,对重构没有系统性、全局性的认识,面对一堆烂代码,没有重构技巧的指导,只能想到哪改到哪,并不能全面地改善代码质量。
为了让你对重构有个清晰的认识,对于这部分知识的讲解,我安排了六节课的内容,主要包含以下几个方面:
对重构概括性的介绍,包括重构的目的(why)、对象(what)、时机(when)、方法(how);
保证重构不出错的手段,这里我会重点讲解单元测试和代码的可测试性;
不同规模的重构,重点讲解大规模高层次重构(比如系统、模块、代码结构、类与类之间的交互等的重构)和小规模低层次重构(类、函数、变量等的重构)。
话不多说,现在就让我们来学习第一部分内容:重构的目的、对象、时机和方法。
# 重构的目的:为什么要重构(why)?
虽然对于你来说,重构这个词可能不需要过多解释,但我们还是简单来看一下,大师是怎么描述它的。软件设计大师MartinFowler是这样定义重构的:“重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。”
实际上,当讲到重构的时候,很多书籍都会引用这个定义。这个定义中有一个值得强调的点:“重构不改变外部的可见行为”。我们可以把重构理解为,在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量。
简单了解重构的定义之后,我们重点来看一下,为什么要进行代码重构?
首先,重构是时刻保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。项目在演进,代码不停地在堆砌。如果没有人为代码的质量负责任,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。
其次,优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。我们无法100%遇见未来的需求,也没有足够的精力、时间、资源为遥远的未来买单,所以,随着系统的演进,重构代码也是不可避免的。
最后,重构是避免过度设计的有效手段。在我们维护代码的过程中,真正遇到问题的时候,再对代码进行重构,能有效避免前期投入太多时间做过度的设计,做到有的放矢。
除此之外,重构对一个工程师本身技术的成长也有重要的意义。
从前面我给出的重构的定义来看,重构实际上是对我们学习的经典设计思想、设计原则、设计模式、编程规范的一种应用。重构实际上就是将这些理论知识,应用到实践的一个很好的场景,能够锻炼我们熟练使用这些理论知识的能力。除此之外,平时堆砌业务逻辑,你可能总觉得没啥成长,而将一个比较烂的代码重构成一个比较好的代码,会让你很有成就感。
除此之外,重构能力也是衡量一个工程师代码能力的有效手段。所谓“初级工程师在维护代码,高级工程师在设计代码,资深工程师在重构代码”,这句话的意思是说,初级工程师在已有代码框架下修改bug、修改添加功能代码;高级工程师从零开始设计代码结构、搭建代码框架;而资深工程师为代码质量负责,需要发觉代码存在的问题,重构代码,时刻保证代码质量处于一个可控的状态(当然这里的初级、高级、资深只是一个相对概念,并不是一个确定的职级)。
# 重构的对象:到底重构什么(what)?
根据重构的规模,我们可以笼统地分为大规模高层次重构(以下简称为“大型重构”)和小规模低层次的重构(以下简称为“小型重构”)。
大型重构指的是对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入bug的风险也会相对比较大。
小型重构指的是对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名、规范注释、消除超大类或函数、提取重复代码等等。小型重构更多的是利用我们能后面要讲到的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入bug的风险相对来说也会比较小。你只需要熟练掌握各种编码规范,就可以做到得心应手。
关于具体如何来做大型重构和小型重构,我会在后面的课程中详细讲解。
# 重构的时机:什么时候重构(when)?
搞清楚了为什么重构,到底重构什么,我们再来看一下,什么时候重构?是代码烂到一定程度之后才去重构吗?当然不是。因为当代码真的烂到出现“开发效率低,招了很多人,天天加班,出活却不多,线上bug频发,领导发飙,中层束手无策,工程师抱怨不断,查找bug困难”的时候,基本上重构也无法解决问题了。
我个人比较反对,平时不注重代码质量,堆砌烂代码,实在维护不了了就大刀阔斧地重构、甚至重写的行为。有时候项目代码太多了,重构很难做得彻底,最后又搞出来一个“四不像的怪物”,这就更麻烦了!所以,寄希望于在代码烂到一定程度之后,集中重构解决所有问题是不现实的,我们必须探索一条可持续、可演进的方式。
所以,我特别提倡的重构策略是持续重构。这也是我在工作中特别喜欢干的事情。平时没有事情的时候,你可以看看项目中有哪些写得不够好的、可以优化的代码,主动去重构一下。或者,在修改、添加某个功能代码的时候,你也可以顺手把不符合编码规范、不好的设计重构一下。总之,就像把单元测试、CodeReview作为开发的一部分,我们如果能把持续重构也作为开发的一部分,成为一种开发习惯,对项目、对自己都会很有好处。
尽管我们说重构能力很重要,但持续重构意识更重要。我们要正确地看待代码质量和重构这件事情。技术在更新、需求在变化、人员在流动,代码质量总会在下降,代码总会存在不完美,重构就会持续在进行。时刻具有持续重构意识,才能避免开发初期就过度设计,避免代码维护的过程中质量的下降。而那些看到别人代码有点瑕疵就一顿乱骂,或者花尽心思去构思一个完美设计的人,往往都是因为没有树立正确的代码质量观,没有持续重构意识。
# 重构的方法:又该如何重构(how)?
前面我们讲到,按照重构的规模,重构可以笼统地分为大型重构和小型重构。对于这两种不同规模的重构,我们要区别对待。
对于大型重构来说,因为涉及的模块、代码会比较多,如果项目代码质量又比较差,耦合比较严重,往往会牵一发而动全身,本来觉得一天就能完成的重构,你会发现越改越多、越改越乱,没一两个礼拜都搞不定。而新的业务开发又与重构相冲突,最后只能半途而废,revert掉所有的改动,很失落地又去堆砌烂代码了。
在进行大型重构的时候,我们要提前做好完善的重构计划,有条不紊地分阶段来进行。每个阶段完成一小部分代码的重构,然后提交、测试、运行,发现没有问题之后,再继续进行下一阶段的重构,保证代码仓库中的代码一直处于可运行、逻辑正确的状态。每个阶段,我们都要控制好重构影响到的代码范围,考虑好如何兼容老的代码逻辑,必要的时候还需要写一些兼容过渡代码。只有这样,我们才能让每一阶段的重构都不至于耗时太长(最好一天就能完成),不至于与新的功能开发相冲突。
大规模高层次的重构一定是有组织、有计划,并且非常谨慎的,需要有经验、熟悉业务的资深同事来主导。而小规模低层次的重构,因为影响范围小,改动耗时短,所以,只要你愿意并且有时间,随时都可以去做。实际上,除了人工去发现低层次的质量问题,我们还可以借助很多成熟的静态代码分析工具(比如CheckStyle、FindBugs、PMD),来自动发现代码中的问题,然后针对性地进行重构优化。
对于重构这件事情,资深的工程师、项目leader要负起责任来,没事就重构一下代码,时刻保证代码质量处在一个良好的状态。否则,一旦出现“破窗效应”,一个人往里堆了一些烂代码,之后就会有更多的人往里堆更烂的代码。毕竟往项目里堆砌烂代码的成本太低了。不过,保持代码质量最好的方法还是打造一种好的技术氛围,以此来驱动大家主动去关注代码质量,持续重构代码。
# 重点回顾
今天的讲解比较偏理论、偏思想教育,主要还是让你对重构有个正确的、全局性的认知,建立持续重构意识。我觉得,这可能比教会你一些重构技巧更重要,因为很多技术问题本身就不是单纯靠技术来解决的,更重要的是要有这种认知和意识。
好了,下面我们还是来总结一下。对于今天的内容,你需要重点理解并且掌握如下知识点。
1.重构的目的:为什么重构(why)?
对于项目来言,重构可以保持代码质量持续处于一个可控状态,不至于腐化到无可救药的地步。对于个人而言,重构非常锻炼一个人的代码能力,并且是一件非常有成就感的事情。它是我们学习的经典设计思想、原则、模式、编程规范等理论知识的练兵场。
2.重构的对象:重构什么(what)?
按照重构的规模,我们可以将重构大致分为大规模高层次的重构和小规模低层次的重构。大规模高层次重构包括对代码分层、模块化、解耦、梳理类之间的交互关系、抽象复用组件等等。这部分工作利用的更多的是比较抽象、比较顶层的设计思想、原则、模式。小规模低层次的重构包括规范命名、注释、修正函数参数过多、消除超大类、提取重复代码等等编程细节问题,主要是针对类、函数级别的重构。小规模低层次的重构更多的是利用编码规范这一理论知识。
3.重构的时机:什么时候重构(when)?
我反复强调,我们一定要建立持续重构意识,把重构作为开发必不可少的部分,融入到日常开发中,而不是等到代码出现很大问题的时候,再大刀阔斧地重构。
4.重构的方法:如何重构(how)?
大规模高层次的重构难度比较大,需要组织、有计划地进行,分阶段地小步快跑,时刻让代码处于一个可运行的状态。而小规模低层次的重构,因为影响范围小,改动耗时短,所以,只要你愿意并且有时间,随时随地都可以去做。
# 课堂讨论
今天课堂讨论的话题是:关于代码重构,你有什么心得体会、经验教训?或者,你也可以说说,在重构过往项目的时候,你遇到过哪些问题?
欢迎在留言区写下你的答案,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。
# 精选评论
点击查看
重构最难的还是领导不支持
前段时间刚重构了一个功能模块。该模块可以说是祖传代码。里面堆砌着各种判断条件,就是所谓的箭头型代码。我接手这个功能重构的 1.把代码读一遍和跑一遍,理解里面的需求。尽量画一个流程图。 2.建立防护网。将需求拆分之后,针对每个拆分的业务点写单元测试。 4.开始重构,解耦逻辑,设计方法的时候尽量让职业单一,类与类之间尽量符合迪米特原则,有依赖关系的类尽量只依赖类的特定方法。我觉得比较基础也是比较重要一点。不要有重复代码。命名要规范,类的各个职业要清晰。重构过程中,其实也要时不时的识别代码的坏味道。尽然是重构,那么肯定要比不重构之前肯定要更好。 5.重构完成之后,通过防护网的测试。
当天重构代码上线之后,基本上没有问题。运行了几天之后有一小段逻辑隐藏的比较深没有写这个逻辑测试,后面补上了一直都没有出过问题了。还是比较稳定的。 我这里只是做了中小规模的重构,后面跟着小争哥继续系统的学习大规模重构,以及重构的技巧和思想。
重构自然是要用的我们牛逼的设计模式和数据结构了。。。啊-_-||开个玩笑哈。
重构这玩意嘛,其实在第一版提上去的时候就应该要重构了,也就是我们常说的,边写边重构。
一个方法写的时候发现分支判断太多,工厂模式就要登场了。
如果大部分代码都比较重复,这个时候就需要往底层的抽象,甚至用上策略模式。
需要做一个非功能性需求,每一个接口调用都要记录的东西,我们为了避免业务侵入性,就要考虑代理模式重构之前的业务侵入性强的代码,将功能与非功能分离。
说到底,重构不要等,而是马上动手,只有行动了,才不会害怕。第一版稍微辛苦一些,以后就不会那么恶心了,功在当代,利在千秋。
代码中的坏味道,好比人的头疼脑热。“小病”不管的话,迟早会发展成大病,需要动大手术,甚至病入膏肓。 实际中的一些体会: 一、在完成一个新需求时,在时间允许的情况下,会经常改进代码,使代码更优雅。 二、“重构不改变外部的可见行为“,引入自动化测试非常重要,国内有些团队可能做的不好。因为改动代码可能引入bug,如果没有自动化测试,测起来就会非常费劲,改动的结果不确定。如果测试不方便,谁会愿意修改之前work的代码呢? 三、持续集成、自动化测试、持续重构都是很好的工程实践。即使工作的项目中暂时没有使用,也应该有所了解。
1,无单测的条件下,别说重构了,我不想以前任何代码,对,我是一个怕事的程序员~ 2,小步快走的重构方式很重要,毕其功于一役的重构总是构完了,发现和主干代码相差哈哈哈,我还是不合入了吧,留给自己欣赏自己精细雕琢的玩具。。。
重构的经验,1.工作中鼓励持续重构,但不赞成为了重构而重构.2.重构一定要在有比较完善的测试用例覆盖和回归用例库的情况下进行(可测试性),否则会相当危险。3.重构最好有AB工具灰度比对,逐步切流。4.重构最好有资深的成员共同CR,结合大家的意见,可能本次的重构也会引入一些怪味道。 重构的教训,出现问题的场景往往在于一个细小的点,能注意到的往往没有问题。重构一旦出问题会面临比较大的精神压力和信心挑战,会部分挫败重构者的积极性,这时候需要TL的鼓励和支持,避免让员工感受到做多错多。
在农村长大的孩子应该多少见过盖房子的情形,一般师傅会用砖挂一根线在不断的盖房子的过程中观察整个房子是否出现歪斜的情况,这个过程是持续的,要时刻保证一砖一瓦的建上去的房子是笔直的。写代码就是如此,团队应该有这样的"一根线"来保证产品的正常开发,不至于整个项目出现问题,而重构就像是发现了房子有点歪需要重新进行改正,高手是发现绳子偏移的时候就开始纠错了,大部分团队只能等到明显发现房子歪了才开始修正。
1.座右铭:你写的每一行代码,都是你的名片。
2.重构要考虑时机和力度。一般是增加需求时,对关联的逻辑代码做的重构。这时需要考虑自己当前的开发期限去决定重构的力度。在保证“营地比自己来时干净”的前提下,量时重构。(逻辑级别小重构一般就如栏主说的改变量名,方法名,以及不破坏现有逻辑做实现代码实现重构,移动变量,内联提炼啥的)(逻辑级别的大重构就要动实现逻辑,甚至需求设计了。往往需要再三与产品沟通确认,并充分测试,甚至在实现上留有开关,一旦现网有问题,及时切换)(架构级别的大重构,包括调整分层模型,重新划分各个微服务持有的聚合,基础技术栈升级,比如spring到springboot,或则jdk。这些影响面都比较大,很难测试全,所以一般是并行重构,然后走现网镜像流量持续观察,大面积业务场景没问题后再整个切换)
3.持续重构锻炼架构思维,受益匪浅。重构不难,难的是在代码上讲究的意愿。
设计模式_27
没有做过大重构,小重构倒是经常做,我发现我做需求常常比别人慢,原因是大概率我改的地方比需求涉及的多一些,可以算是小重构吧。
很同意王争老师说的,大部分人都没有进行过重构,而大部分的代码又是由这大部分的人写的,所以,市面上的大部分代码可能都。。。
我在创业公司呆过,见识过技术总监申请重构时被挑战的体无完肤,不欢而散,代码一直烂下去,最终谁也改不动,大量的客户流失,业务严重受损。当然,我在这个过程中,毫无作为。。。
分享一个我们团队的案例:某个产品线经过重构,崩溃率降低了50%。 其实那一次我们团队遇到的最大问题是:相关负责人“不敢”动前任遗留的烂代码。后来无奈“强迫”那个同事去做,效果立竿见影,之后很快就把“重构”推广到了其他项目。再之后整个团队开始重视代码质量,有坏味道随时重构,整个团队慢慢进入了一个“正循环”。
庆幸自己有个好领导,日常对我的代码review得非常严格,平时使用sourcetree,gitrebase可以清晰地看到每一次提交,这样代码review起来就没什么压力了。
另一方面,最好在项目的开始阶段和大家分享你的设计思路,否则等项目要准备发布上线时,拉上一堆人来review代码时,其实效果几乎没啥的,大家只能看看命名风格,像什么高内聚低耦合,设计模式,封装抽象等即使有问题,也会因为项目时间紧而将就放行。
我现在负责的项目是我从零就参与的,到现在大大小小已经迭代了十几个版本。我发现随着版本的迭代,会出现很多相似的重复代码,这个时候我就会去想办法重构代码,做一些抽象啊,利用一些设计模式,不过我现在只用到了模板设计模式。如果不重构我觉着以后需求再变化改动的地方太多了,而且还很有可能出错。另外,我发现重构了以后代码的可读性也会比较好。
品味很重要,能品味出代码的味道
打卡 一、重构的目的: 定义:“重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。” 1、在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量。 2、时刻保证代码质量的一个有效手段 3、优秀的架构或是代码不是一开始就能设计好的,需要持续演进 4、避免前期的过度设计 5、重构是对我们的学习的经典的设计思想、设计原则、设计模式、编程规范的一个综合应用 二、重构的对象 1、大型重构:对代码结构、模块、类与之间的关系、 大型重构的手段:分层、模块化、解耦、抽象可复用组件等 2、小规模低层次重构:针对代码细节重构 小型重构的手段:针对类、函数、变量等代码级别的重构 三、重构的时机: 融入到日常开发中 三、重构的方法: 大规模:有组织、有计划、分阶段小步快跑 小规模:随时进行
在项目开发中,对于已经运行了五六年的系统,我们其实没什么经验去做重构的,加个功能都可能搞坏;不过我的老板对于核心的模块的质量确实是在持续重构,他看到代码有重构的机会的时候就会立即下手,我应该向他学习这种态度,并努力锻炼出持续重构的实力。
边写边改