前言: 捧读像这一类的书对于本身来讲总带着一些神圣感,感谢本身并无被这么宏大的主题吓退,看完了这里分享输出一下本身的笔记。javascript
按书中 P45 中的说法,重构这个概念被分红了动词和名词的方面被分别阐述:java
在过去的几十年时间里,重构这个词彷佛被用来代指任何形式的代码清理,但上面的定义所指的是一种特定的清理代码的方式。重构的关键在于运用大量微小且保持软件行为的步骤,一步一步达成大规模的修改。git
每一次的重构要么很小,要么包含了若干个小步骤,即便重构没有完成,也应当能够在任什么时候刻停下来,因此若是有人说它们的代码在重构过程当中有一两天时间不可用,基本上能够肯定,他们作的事不是重构。程序员
重构与性能优化有不少类似的地方:二者都须要修改代码,而且二者都不会改变程序的总体功能。github
二者的差异在于起目的:编程
重构不是包治百病的灵丹妙药,也绝对不是所谓的“银弹”。重构只是一种工具,可以帮助你始终良好的控制代码而已。使用它,可能基于下面的几个目的。性能优化
这里有一个有意思的科普(引用自百度百科:没有银弹
):在民俗传说里,全部能让咱们充满梦靥的怪物之中,没有比狼人更可怕的了,由于它们会忽然地从通常人变身为恐怖的怪兽,所以人们尝试着查找可以奇迹似地将狼人一枪毙命的银弹。咱们熟悉的软件项目也有相似的特质(以一个不懂技术的管理者角度来看),日常看似单纯而率直,但极可能一转眼就变成一只时程延误、预算超支、产品充满瑕疵的怪兽,因此,咱们听到了绝望的呼唤,渴望有一种银弹,可以有效下降软件开发的成本,就跟电脑硬件成本能快速降低同样。微信
当人们只为短时间目的而修改代码时,他们常常没有彻底理解架构的总体设计。因而代码逐渐失去了本身的结构。程序员愈来愈难以经过阅读代码来理解原来的设计。代码结构的流失有累积效应。越难看出代码所表明的设计企图,就越难以保护其设计,因而设计就腐败得越快。架构
完成一样一件事,设计欠佳的程序每每须要更多代码,这经常是由于代码在不一样的地方使用彻底相同的语句作一样的事情,所以改进设计的一个重要方向就是消除重复代码。消除重复代码,我就能够肯定全部事物和行为在代码中只表述一次,这正是优秀设计的根本。编程语言
所谓程序设计,很大程度上就是与计算机对话:我编写代码告诉计算机作什么,而它的响应是按照个人指示精确行动。一言以蔽之,我所作的就是填补“我想要它作什么”和“我告诉它作什么”之间的缝隙。编程的核心就在于“准确说出我想要的”。
然而别忘了,除计算机以外,源码还有其余读者,而且很大几率仍是几个月后的本身,如何更清晰地表达我想要作的,这可能就须要一些重构的手法。
这里我联想到了软件设计的 KISS 原则:KISS 原则,Keep It Simple and Stupid ,简单的理解这句话就是,要把一个系统作的连白痴都会用。
对代码的理解,能够帮助找到系统中存在的一些 BUG。搞清楚程序结构的同时,也能够对本身的假设作一些验证,这样一来 BUG 想不发现都难。
Kent Beck 常常形容本身的一句话是:“我不是一个特别好的程序员,我只是一个有着一些特别好的习惯的还不错的的程序员。”重构可以帮助咱们更有效地写出健壮的代码。
听起来可能有些反直觉,由于重构可能会花大量的时间改善设计、提升阅读性、修改 BUG,难道不是在下降开发速度嘛?
软件开发者交谈时的故事:一开始他们进展很快,但现在想要添加一个新功能须要的时间就要长得多。他们须要花愈来愈多的时间去考虑如何把新功能塞进现有的代码库,不断蹦出来的bug修复起来也愈来愈慢。代码库看起来就像补丁摞补丁,须要细致的考古工做才能弄明白整个系统是如何工做的。这份负担不断拖慢新增功能的速度,到最后程序员巴不得从头开始重写整个系统。
下面这幅图能够描绘他们经历的困境。
但有些团队的境遇则大相径庭。他们添加新功能的速度愈来愈快,由于他们能利用已有的功能,基于已有的功能快速构建新功能。
两种团队的区别就在于软件的内部质量。须要添加新功能时,内部质量良好的软件让我能够很容易找到在哪里修改、如何修改。良好的模块划分使我只须要理解代码库的一小部分,就能够作出修改。若是代码很清晰,我引入 BUG 的可能性就会变小,即便引入了 BUG,调试也会容易得多。理想状况下,代码库会逐步演化成一个平台,在其上能够很容易地构造与其领域相关的新功能。
这种现象被做者称为“设计耐久性假说”:经过投入精力改善内部设计,咱们增长了软件的耐久性,从而能够更长时间地保持开发的快速。目前还没法科学地证实这个理论,因此说它是一个“假说”。
20年前,行业的陈规认为:良好的设计必须在开始编程以前完成,由于一旦开始编写代码,设计就只会逐渐腐败。重构改变了这个图景。如今咱们能够改善已有代码的设计,所以咱们能够先作一个设计,而后不断改善它,哪怕程序自己的功能也在不断发生着变化。因为预先作出良好的设计很是困难,想要既体面又快速地开发功能,重构必不可少。
重构并非必要,固然也有一些不那么须要重构的状况:
重构的最佳时机就在添加新功能以前。在动手添加新功能以前,我会看看现有的代码库,此时常常会发现:若是对代码结构作一点微调,个人工做会容易得多。也许已经有个函数提供了我须要的大部分功能,但有几个字面量的值与个人须要略有冲突。若是不作重构,我可能会把整个函数复制过来,修改这几个值,但这就会致使重复代码—若是未来我须要作修改,就必须同时修改两处(更麻烦的是,我得先找到这两处)。并且,若是未来我还须要一个相似又略有不一样的功能,就只能再复制粘贴一次,这可不是个好主意。
这就好像我要往东去100千米。我不会往东一头把车开进树林,而是先往北开20千米上高速,而后再向东开100千米。后者的速度比前者要快上3倍。若是有人催着你“赶快直接去那儿”,有时你须要说:“等等,我要先看看地图,找出最快的路径。”这就是预备性重构于个人意义。
——Jessica Kerr
修复bug时的状况也是同样。在寻找问题根因时,我可能会发现:若是把3段如出一辙且都会致使错误的代码合并到一处,问题修复起来会容易得多。或者,若是把某些更新数据的逻辑与查询逻辑分开,会更容易避免形成错误的逻辑纠缠。用重构改善这些状况,在一样场合再次出现一样bug的几率也会下降。
我须要先理解代码在作什么,而后才能着手修改。这段代码多是我写的,也多是别人写的。一旦我须要思考“这段代码到底在作什么”,我就会自问:能不能重构这段代码,令其一目了然?我可能看见了一段结构糟糕的条件逻辑,也可能但愿复用一个函数,但花费了几分钟才弄懂它到底在作什么,由于它的函数命名实在是太糟糕了。这些都是重构的机会。
看代码时,我会在脑海里造成一些理解,但个人记性很差,记不住那么多细节。正如 Ward Cunningham 所说,经过重构,我就把脑子里的理解转移到了代码自己。随后我运行这个软件,看它是否正常工做,来检查这些理解是否正确。若是把对代码的理解植入代码中,这份知识会保存得更久,而且个人同事也能看到。
重构带来的帮助不只发生在未来——经常是立竿见影。是我会先在一些小细节上使用重构来帮助理解,给一两个变量更名,让它们更清楚地表达意图,以方便理解,或是将一个长函数拆成几个小函数。当代码变得更清晰一些时,我就会看见以前看不见的设计问题。若是不作前面的重构,我可能永远都看不见这些设计问题,由于我不够聪明,没法在脑海中推演全部这些变化。Ralph Johnson说,这些初步的重构就像扫去窗上的尘埃,使咱们得以看到窗外的风景。在研读代码时,重构会引领我得到更高层面的理解,若是只是阅读代码很难有此领悟。有些人觉得这些重构只是毫无心义地把玩代码,他们没有意识到,缺乏了这些细微的整理,他们就没法看到隐藏在一片混乱背后的机遇。
帮助理解的重构还有一个变体:我已经理解代码在作什么,但发现它作得很差,例如逻辑没必要要地迂回复杂,或者两个函数几乎彻底相同,能够用一个参数化的函数取而代之。这里有一个取舍:我不想从眼下正要完成的任务上跑题太多,但我也不想把垃圾留在原地,给未来的修改增长麻烦。若是我发现的垃圾很容易重构,我会立刻重构它;若是重构须要花一些精力,我可能会拿一张便笺纸把它记下来,完成当下的任务再回来重构它。
固然,有时这样的垃圾须要好几个小时才能解决,而我又有更紧急的事要完成。不过即使如此,稍微花一点工夫作一点儿清理,一般都是值得的。正如野营者的老话所说:至少要让营地比你到达时更干净。若是每次通过这段代码时都把它变好一点点,聚沙成塔,垃圾总会被处理干净。重构的妙处就在于,每一个小步骤都不会破坏代码——因此,有时一块垃圾在好几个月以后才终于清理干净,但即使每次清理并不完整,代码也不会被破坏。
上面的例子——预备性重构、帮助理解的重构、捡垃圾式重构——都是见机行事的:我并不专门安排一段时间来重构,而是在添加功能或修复 BUG 的同时顺便重构。这是我天然的编程流的一部分。无论是要添加功能仍是修复 BUG,重构对我当下的任务有帮助,并且让我将来的工做更轻松。这是一件很重要而又常被误解的事:重构不是与编程割裂的行为。你不会专门安排时间重构,正如你不会专门安排时间写 if
语句。个人项目计划上没有专门留给重构的时间,绝大多数重构都在我作其余事的过程当中天然发生。
还有一种常见的误解认为,重构就是人们弥补过去的错误或者清理肮脏的代码。固然,若是赶上了肮脏的代码,你必须重构,但漂亮的代码也须要不少重构。在写代码时,我会作出不少权衡取舍:参数化须要作到什么程度?函数之间的边界应该划在哪里?对于昨天的功能彻底合理的权衡,在今天要添加新功能时可能就再也不合理。好在,当我须要改变这些权衡以反映现实状况的变化时,整洁的代码重构起来会更容易。
长久以来,人们认为编写软件是一个累加的过程:要添加新功能,咱们就应该增长新代码。但优秀的程序员知道,添加新功能最快的方法每每是先修改现有的代码,使新功能容易被加入。因此,软件永远不该该被视为“完成”。每当须要新能力时,软件就应该作出相应的改变。越是在已有代码中,这样的改变就越显重要。
不过,说了这么多,并不表示有计划的重构老是错的。若是团队过去忽视了重构,那么经常会须要专门花一些时间来优化代码库,以便更容易添加新功能。在重构上花一个星期的时间,会在将来几个月里发挥价值。有时,即使团队作了平常的重构,仍是会有问题在某个区域逐渐累积长大,最终须要专门花些时间来解决。但这种有计划的重构应该不多,大部分重构应该是不起眼的、见机行事的。
大多数重构能够在几分钟—最多几小时—内完成。但有一些大型的重构可能要花上几个星期,例如要替换一个正在使用的库,或者将整块代码抽取到一个组件中并共享给另外一支团队使用,再或者要处理一大堆混乱的依赖关系,等等。
即使在这样的状况下,我仍然不肯让一支团队专门作重构。可让整个团队达成共识,在将来几周时间里逐步解决这个问题,这常常是一个有效的策略。每当有人靠近“重构区”的代码,就把它朝想要改进的方向推进一点。这个策略的好处在于,重构不会破坏代码—每次小改动以后,整个系统仍然照常工做。例如,若是想替换掉一个正在使用的库,能够先引入一层新的抽象,使其兼容新旧两个库的接口。一旦调用方已经彻底改成使用这层抽象,替换下面的库就会容易得多。(这个策略叫做Branch By Abstraction[mf-bba]。)
至于如何在代码复审的过程当中加入重构,这要取决于复审的形式。在常见的pull request模式下,复审者独自浏览代码,代码的做者不在旁边,此时进行重构效果并很差。若是代码的原做者在旁边会好不少,由于做者能提供关于代码的上下文信息,而且充分认同复审者进行修改的意图。对我我的而言,与原做者肩并肩坐在一块儿,一边浏览代码一边重构,体验是最佳的。这种工做方式很天然地导向结对编程:在编程的过程当中持续不断地进行代码复审。
这让我想起以前在捧读《阿里巴巴 Java 开发手册》时学习的代码规范的问题(传送门)
,只不过当时学习的是好的代码应该长什么样,而如今讨论的事情是:坏的代码长什么样?
其实大部分的状况应该做为程序员的咱们都有必定的共识,因此我以为简单列一下书中提到的状况就足以说明:
神秘命名
重复代码
过长函数
过长参数列表
全局数据: 全局数据的问题在于,从代码库的任何一个角落均可以修改它,并且没有任何机制能够探测出到底哪段代码作出了修改。一次又一次,全局数据形成了一些诡异的 BUG,而问题的根源却在遥远的别处。
可变数据: 对数据的修改常常致使出乎意料的结果和难以发现的 BUG。我在一处更新数据,却没有意识到软件中的另外一处指望着彻底不一样的数据。
发散式变化: 模块常常由于不一样的缘由在不一样的方向上发生变化。
散弹式修改: 每遇到某种变化,你都必须在许多不一样的类内作出许多小修改。
依恋情结: 所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。但有时你会发现,一个函数跟另外一个模块中的函数或者数据交流格外频繁,远胜于在本身所处模块内部的交流。
数据泥团: 你常常在不少地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。
基本类型偏执: 不少程序员不肯意建立对本身的问题域有用的基本类型,如钱、坐标、范围等。
重复的 switch: 在不一样的地方反复使用相同的 switch 逻辑。问题在于:每当你想增长一个选择分支时,必须找到全部的 switch,并逐一更新。
循环语句: 咱们发现,管道操做(如 filter 和 map)能够帮助咱们更快地看清被处理的元素一级处理它们的动做。
冗余的元素
夸夸其谈通用性: 函数或类的惟一用户是测试用例。
临时字段: 有时你会看到这样的类:其内部某个字段仅为某种特定状况而定。这样的代码让人不理解,由于你一般认为对象在全部时候都须要它的全部字段。在字段未被使用的状况下猜想当初设置它的目的,会让你发疯。
过长的消息链
中间人: 过分运用委托。
内幕交易: 软件开发者喜欢在模块之间筑起高墙,极其反感在模块之间大量交换数据,由于这会增长模块间的耦合。在实际状况里,必定的数据交换不可避免,但咱们必须尽可能减小这种状况,并把这种交换都放到明面上来。
过大的类
殊途同归的类
纯数据类: 所谓纯数据类是指:他们拥有一些字段,以及用于访问(读写)这些字段的函数,除此以外一无长物。纯数据类经常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使状况大为改观。
被拒绝的遗赠: 拒绝继承超类的实现,咱们不介意:但若是拒绝支持超类的接口,这就难以接受了。
注释: 当你感受须要纂写注释时,请先尝试重构,试着让全部注释都变得多余。
书中花了大量的章节介绍咱们应该如何重构咱们的程序,有几个关键的点是我本身可以提炼出来的:找出代码中不合理的地方、结构化、容易理解、测试确保正确。总之围绕这几个点,书中介绍了大量的方法,下面结合本身的一些理解来简单概述一下吧。
结构化的代码更加便于咱们阅读和理解,例如最常使用的重构方法:提炼函数
void printOwing(double amount) { printBanner(); //print details System.out.println ("name:" + _name); System.out.println ("amount" + amount); }
=>
void printOwing(double amount) { printBanner(); printDetails(amount); } void printDetails (double amount) { System.out.println ("name:" + _name); System.out.println ("amount" + amount); }
要保持软件的 KISS 原则是不容易的,可是也有一些方法能够借鉴,例如:引入解释性变量
动机:用一个良好命名的临时变量来解释对应条件子句的意义,使语义更加清晰。
if ( (platform.toUpperCase().indexOf("MAC") > -1) && (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize > 0 ) { // do something }
=>
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1; final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1; final boolean wasResized = resize > 0; if (isMacOs && isIEBrowser && wasInitialized() && wasResized) { // do something }
另外因为 lambda 表达式的盛行,咱们如今有一些更加优雅易读的方法使咱们的代码保持可读:以管道取代循环就是这样一种方法。
const names = []; for (const i of input) { if (i.job === "programer") names.push(i.name); }
=>
const names = input .filter(i => i.job === "programer") .map(i => i.name) ;
例如上面介绍的提炼函数的方法,当然是一种很好的方式,但也应该避免过分的封装,若是别人使用了太多间接层,使得系统中的全部函数都彷佛只是对另外一个函数的简单委托(delegation),形成我在这些委托动做之间晕头转向,而且内部代码和函数名称一样清晰易读,那么就应该考虑内联函数。
动机:①去处没必要要的间接性;②能够找出有用的间接层。
int getRating() { return (moreThanFiveLateDeliveries()) ? 2 : 1; } boolean moreThanFiveLateDeliveries() { return _numberOfLateDeliveries > 5; }
=>
int getRating() { return (_numberOfLateDeliveries > 5) ? 2 : 1; }
封装可以帮助咱们隐藏细节而且,可以更好的应对变化,当咱们发现咱们的类太大而不容易理解的时候,能够考虑使用提炼类的方法。
动机:类太大而不容易理解。
class Person { get officeAreaCode() { return this._officeAreaCode; } get officeNumber() { return this._officeNumber; } }
=>
class Person { get officeAreaCode() { return this._telephoneNumber.areaCode; } get officeNumber() { return this._telephoneNumber.number; } } class TelephoneNumber { get areaCode() { return this._areaCode; } get number() { return this._number; } }
反过来,若是咱们发现一个类再也不承担足够责任,再也不有单独存在的理由的时候,咱们会进行反向重构:内敛类
class Person { get officeAreaCode() { return this._telephoneNumber.areaCode; } get officeNumber() { return this._telephoneNumber.number; } } class TelephoneNumber { get areaCode() { return this._areaCode; } get number() { return this._number; } }
=>
class Person { get officeAreaCode() { return this._officeAreaCode; } get officeNumber() { return this._officeNumber; } }
分解条件式: 咱们能经过提炼代码,把一段 「复杂的条件逻辑」 分解成多个独立的函数,这样就能更加清楚地表达本身的意图。
if (date.before (SUMMER_START) || date.after(SUMMER_END)) charge = quantity * _winterRate + _winterServiceCharge; else charge = quantity * _summerRate;
=>
if (notSummer(date)) charge = winterCharge(quantity); else charge = summerCharge (quantity);
另一个比较受用的一条建议就是:以卫语句取代嵌套条件式。根据经验,条件式一般有两种呈现形式。第一种形式是:全部分支都属于正常行为。第二种形式则是:条件式提供的答案中只有一种是正常行为,其余都是不常见的状况。
精髓是:给某一条分支以特别的重视。若是使用 if-then-else
结构,你对 if
分支和 else
分支的重视是同等的。 这样的代码结构传递给阅读者的消息就是:各个分支有一样的重要性。卫语句(guard clauses)就不一样了,它告诉阅读者:「这种状况很罕见,若是它真的发生了,请作 一些必要的整理工做,而后退出。」
「每一个函数只能有一个入口和一个出口」的观念,根深蒂固于某些程序员的脑海里。 我发现,当我处理他们编写的代码时,我常常须要使用 Replace Nested Conditional with Guard Clauses。现今的编程语言都会强制保证每一个函数只有一个入口, 至于「单一出口」规则,其实不是那么有用。在我看来,保持代码清晰才是最关键的:若是「单一出口」能使这个函数更清楚易读,那么就使用单一出口;不然就没必要这么作。
double getPayAmount() { double result; if (_isDead) result = deadAmount(); else { if (_isSeparated) result = separatedAmount(); else { if (_isRetired) result = retiredAmount(); else result = normalPayAmount(); }; } return result; };
=>
double getPayAmount() { if (_isDead) return deadAmount(); if (_isSeparated) return separatedAmount(); if (_isRetired) return retiredAmount(); return normalPayAmount(); };
若是认真观察程序员把最多时间耗在哪里,你就会发现,编写代码其实只占很是小的一部分。有些时间用来决定下一步干什么,另外一些时间花在设计上面,最多的时间则是用来调试(debug)。每一个程序员都能讲出「花一成天(甚至更多)时间只找出一只小小臭虫」的故事。修复错误一般是比较快的,但找出错误倒是噩梦一场。当你修好一个错误,老是会有另外一个错误出现,并且确定要好久之后才会注意到它。 彼时你又要花上大把时间去寻找它。
「频繁进行测试」是极限编程( extreme programming XP)[Beck, XP]的重要一 环。「极限编程」一词容易让人联想起那些编码飞快、自由而散漫的黑客(hackers), 实际上极限编程者都是十分专一的测试者。他们但愿尽量快速开发软件,而他们也知道「测试」可协助他们尽量快速地前进。
在重构以前,先保证一组可靠的测试用例(有自我检验的能力),这不只有助于咱们检测 BUG,其中也有一种以终为始的思想在里面,实际上,咱们能够经过编写测试用例,更加清楚咱们最终的函数应该长什么样子,提供什么样的服务。
感谢您的耐心阅读,以上就是整个学习的笔记了。
重构不是一个一蹴而就的事,须要长期的实践和经验才可以完成得很好。重构强调的是 Be Better,那在此以前咱们首先须要先动起手来搭建咱们的系统,而不要一味地“完美主义”,近些时间接触的敏捷式开发也正是这样的一种思想。
若是有兴趣阅读,这里只找到一份初版能够在线阅读的地方,请自行食用吧:https://www.kancloud.cn/sstd521/refactor/194190
按照惯例黏一个尾巴:
欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz 欢迎关注公众微信号:wmyskxz 分享本身的学习 & 学习资料 & 生活 想要交流的朋友也能够加qq群:3382693