目录
一、重构概述
任何一个人都可以写出计算机可以理解的程序,只有写出人类容易理解的程序才是优秀的程序员
----- Martin Fowler
1.1、定义
名词形式,对程序内部结构的一种调整,目的是在不改变程序外部行为下,提高其可理解性,降低其修改成本。
动词形式,使用一系列重构准则,在不改变程序外部行为的前提下,对代码作出修改,以改进其内部结构。
1.2、为何重构
改进程序设计,代码结构的流失是累积性的,经常性的重构可以帮助代码维持自己的形态和结构。
使程序更容易理解,重构会使代码渐趋简洁,越简洁就越容易理解,越容易理解就越容易修改。
帮助找到隐藏的Bug,重构过程中,不断深入地理解代码,弄清程序结构,可能就会发现Bug。
提高变成速度,良好的设计是维持软件开发速度的根本。
1.3、何时重构
>>>>>>>>>>>>>>>>>>>>>> 重构应该随时随地的进行 <<<<<<<<<<<<<<<<<<<<<
三次法则,类似的事情,第一次去做时尽管去做,第二次重复做变化产生方案,但是第三次再做时就应该重构了。
添加功能时重构,给代码添加新功能时,不可避免的需要修改旧代码,如果不能快速理解相关代码,就应该重构,这样下次看到更容易理解。
修补错误时重构,当收到错误或者Bug报告的时候,便是需要重构代码的信号,因为代码还不够清晰,没有清晰到一眼就能看出Bug或问题的程度。
Review代码时重构,代码Review对于编写清晰代码很重要,会让更多的人提出更好更有用的建议,对于这些建议可考虑是否可以通过重构来实现它们。
二、什么样的代码需要重构
能够识别出劣质或不友好的代码,是正确重构的前提
2.1、重复的代码
一个类中的两个方法有重复代码,可以通过抽取方法将重复的代码放到另一个方法中供调用。
互为兄弟的子类中如果有重复代码,可以将重复代码抽取到父类中,
两个没有关系的类中如果有重复代码,可以重新抽取一个类将重复代码放到这个类中。
2.2、过长的方法名
使用清晰简洁的命名,如果只是为了给方法起一个好名字而多花了一两分钟开发时间,也是值得的。
2.3、过大的类
类的设计应当遵循单一职责原则(SRP),重构一个巨大的类可以使用抽取接口的方式来搞清楚这个类应该如何拆解。
2.4、过长参数列表
比较常见的是将相关参数组织成一个对象来替换掉这些参数,比如常见开源工具类中线程池的创建或HttpClient的创建等。
2.5、发散式变化
现有一个类,如果先加入一个数据库,必须修改三个方法;如果新出现一个金融工具,必须修改另外四个方法,此时将这个类分成两个类更好。往往只有在加入新数据库或新金融工具后,才会发现这点。
2.6、霰(xian)弹式修改
每遇到变化,需要修改多个类,容易遗漏,应该把需要修改的部分放到一个类中。
发散式变化指“一个类受多种变化的影响”,霰弹式修改则指“一种变化引发多个类响应修改”。
2.7、依恋情结
函数大量地使用了另外类的数据,这种情况下最好将此函数移动到那个类中。
2.8、数据泥团
两个类中大量相同的字段、方法签名中相同的参数等,都适合提取成一个单独的数据类。
2.9、基本类型偏执
如果有大量的基本数据类型字段,就有可能将其中部分存在逻辑联系的字段(类属性或者方法参数)组织起来,形成一个类。更进一步的是,将与这些数据有关联的方法也一并移入类中。
2.10、冗赘类
如果一个类不值得或者不必存在,那么它就应该消失。比如一个类是为了处理“新冠”相关问题的,那等疫情结束了,这个类也就没有存在的必要了。
2.11、Switch语句
一套代码部署在几个环境,在项目中存在许多环境的判断(switch语句实现),不同环境走不同逻辑,如果要添加一个新环境,所有switch语句都要添加一个case。面向对象中“多态”的概念可以优雅的解决这个问题。
可以将switch语句提炼成独立的方法,然后再将此方法搬移到需要“多态”的类里。
2.12、平行继承体系
是“霰弹式修改”的一种特殊情况,每当为某个类增加一个子类,必须也为另一个类相应的增加一个子类。
看一个例子:蜡笔有大、中、小三种型号,12中颜色,那么总共必须有36中蜡笔,每增加一种颜色,都必须增加大、中、小三种型号,颜色和型号紧紧耦合在一起。再来看毛笔,不同的毛笔型号抽象成五种,不同颜色抽象成颜料,毛笔和颜料两个基类形成关联,避免了“霰弹式修改”,这就是Bridge模式。
2.13、夸夸其谈未来性
如果你的抽象类、委托、方法的参数没有实际的作用,那么就应该被移除掉。
比如某个抽象类没有太大的作用,那么就不必抽象这一层。如果方法参数完全没有用到,就应该被移除。还有方法类的唯一调用方是测试用例,应该把方法和测试用例一起删除。
2.14、令人迷惑的暂时字段
类中某个字段只为某些特殊情况而设置。
比如类中有一个复杂的算法,需要好几个变量,为了避免传递过长的参数列表,而把这些变量放到类属性里,但是这些属性只有在使用该算法时才有效,其他情况下会令人迷惑。这时可把变量和相关算法提炼到一个新的类中。
2.15、过度耦合的消息链
常常是因为数据结构的层次很深,需要层层调用getter获取内层数据。如果频繁出现,就应该考虑这个字段是否应该移动到较外层的类,或者把调用链封装在较外层类的方法。
2.16、中间人
对象的基本特征之一就是“封装”,对外部世界隐藏其内部细节,封装往往伴随着“委托”,但是可能会过度运用委托。
如果一个类的很多功能都通过委托给其他类来完成,只是多了一层简单的调用,那么就不如去掉这些中间人直接和真正负责的对象打交道。要是被委托的中间类还有很多其他行为,可以变成子类,扩展原对象的行为。
2.17、亲密关系
两个类太过亲密,比如数据访问权限互相暴露太多,就应该提炼类,将两个类的共同点提炼到新类中,让它们共同使用新类。
继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望,可以运用委托来取代继承。
2.18、异曲同工的类
两个类做类似的事,抽取超类。
2.19、不完美的类库
类库的开发者没有未卜先知的能力,我们经常遇到类库没有我们需要的方法,但是我们又不能直接修改类库里的代码,这时若是只想修改一两个方法,可以引入外加方法,如果想要添加一大堆额外行为,就得引入本地扩展。
2.20、纯稚的数据类
一些数据类型不应该把全部字段单纯的通过getter/setter暴露出来,而应该暴露抽象接口,封装内部结构。
2.21、被拒绝的遗赠
子类不想继承父类的所有方法和数据,只挑选几样来使用,为子类新建一个兄弟类,再运用下移方法和下移字段把用不到的方法下推给兄弟类。
子类只复用父类的行为,却不想支持父类的接口,运用委托替代继承来达到目的。
2.22、过多的注释
一些人常用注释来补救劣质代码,事实上当重构移除所有劣质代码的时候,注释已经变得多余,因为代码已经讲清楚了一切。
三、重构技巧
Java开发,由于IDE(Intellij Idea)能够很好的支持大多数情况下的重构,有各种自动提示,所以写代码的时候要多注意提示,及时消除警告。
3.1、重新组织函数
3.1.1、引入解释性变量
将该复杂表达式的结果放进一个临时变量,以变量名来解释其用途。
3.1.2、分解临时变量
一个临时变量多次被赋值(除了“循环变量”和“结果收集变量”),应该针对每次赋值,创造独立的临时变量。
临时变量会被多次赋值,容易产生理解歧义。
3.1.3、移除对参数的赋值
以一个临时变量取代改参数的位置,对参数赋值容易降低代码的清晰度,容易混淆按值传递和按引用传递的方式。
3.1.4、以卫语句取代嵌套if-else语句
复杂嵌套的条件语句使人难以看清正常的执行路径。
3.1.5、以函数对象取代函数
一个大型函数如果包含了很多临时变量,用Extract Method很难拆解,可以把函数放到一个新创建的类中,把临时变量变成类的实体变量,再用Extract Method拆解。
3.1.6、替换算法
复杂的算法会增加维护的成本,替换成较简单的算法实现,往往能明显提高代码的可读性和可维护性。
3.2、在对象之间搬移特性
3.2.1、移动函数
类的行为做到单一职责,不要越俎代庖。如果一个类有太多行为,或一个类与另一个类有太多合作而形成高度耦合,就需要搬移函数。
3.2.2、搬移字段
如果一个类的字段在另一个类中使用更频繁,就应该考虑搬移它。
3.2.3、提炼类
一个类做了过多的工作,就应该创建一个新的类,将多余特性相关的字段和方法从源类搬移到目标类。一个类应该是一个清晰的抽象,有明确的责任。
3.2.4、将类内联化
与提炼类相反,如果一个类没有做太多事情(没有承担足够责任),就将此类所有特性搬移到另一个类中,删除原类。
3.2.5、隐藏委托关系
委托关系发生变化,变化将被限制在委托类中,使用方感知不到。
3.2.6、移除中间人
封装委托对象也是要付出代价的:每当客户要使用受托类的新特性时,就必须在服务端添加一个委托函数。随着委托类的特性(功能)越来越多,服务类完全变成了“中间人”,此时就应该让客户直接调用受托类。
很难说什么程度的隐藏才是合适的,随着系统不断变化,需要不断调整。
3.2.7、引入外加函数
你需要为提供服务的类增加一个函数,单你无法修改这个类。在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。
Date nextDayDate = new Date(year, month, day + 1);
新增外部函数Date
nextDay(Date date) {return new Date(date.year, date.month, date.day + 1);}
3.2.8、引入本地扩展
你需要为服务类提供一些额外函数,但你无法修改这个类。建立一个新类,使它包含这些额外函数,让这个扩展类成为源类的子类或者包装类。
3.3、重新组织数据
3.3.1、自封装字段
为这个字段建立getter/setter函数,并且只以这些函数访问字段。
直接访问一个字段会导致出现强耦合关系。
直接反问的好处是易阅读,间接访问的好处是好管理以及子类好覆写。
3.3.2、对象取代数据值
一个数据项,需要与其他数据和行为一起使用才有意义。随着设计深入,数据之间的关系逐渐显现出来,就需要将相关数据及其操作封装成对象。
3.3.3、将值对象改为引用对象
如果希望修改某个值对象的数据,并且影响到所有引用此对象的地方,将这个值对象变成引用对象。
3.3.4、以对象取代数组
如果一个数组中的元素各自代表不同的东西,以对象替换数组,对于数组中的每个元素,以一个字段来表示。
3.3.5、将单向关联改为双向关联
两个类都需要使用对方特性,但其间只有一条单向连接。添加一个反向指针,并使修改函数能够同时更新两条连接。
3.3.6、将双向关联改为单向关联
两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性,去除不必要的关联。
3.3.7、封装字段
类中存在一个public字段。将它声明为private,并提供相应的访问函数。
3.4、简化表达式
3.4.1、分解条件表达式
程序中,复杂的条件逻辑是最长导致复杂度上升的地点之一。
可以将它拆解为多个独立函数,根据每个小块代码的用途,为拆解的新函数命名,从而更清楚的表达意图。
3.4.2、合并重复的条件片段
在条件表达式的每个分支上有着相同的一段代码,将这段重复代码移到条件表达式之外。
3.4.3、以卫语句取代嵌套条件表达式
可参看3.1.4的内容
3.4.4、以多态取代条件表达式
一个条件表达式,它根据对象类型的丌(ji)同而选择丌同的行为。将这个条件表达式的每个分支放进一个子类的覆写函数中,然后将原始函数声明为抽象函数。
3.4.5、引入断言
某一段代码需要对程序状态作出某种假设,以断言明确表现出这种假设。
使用断言明确标明对输入条件的严格要求和限制。
断言可以辅助交流和调试。
3.5、简化函数调用
3.5.1、函数改名
将名字过于复杂的函数进行简化
3.5.2、添加参数
某个函数需要从调用端得到跟多信息,为此函数添加一个对象参数,让该对象带进函数所需信息。
3.5.3、移除参数
当函数不再需要某个参数时,将其移除。
3.5.4、将查询函数和修改函数分离
3.5.5、以明确函数取代参数
某个函数完全取决于参数值而采取不同行为,为了获得一个清晰的接口,针对该参数的每一个可能值,建立一个独立函数。
3.5.6、保持对象完整
如果从对象中取出若干值,将它们作为某一次函数调用时的参数,改为传递整个对象。除了可以使参数列更稳固外,还能简化参数列表,提高代码的可读性。此外,使用完整对象,被调用函数可以利用完整对象中的函数来计算某些中间值。
如果这些使对象间依赖结构恶化,就不该使用。
3.5.7、引入参数对象
如果一组参数总是一起被传递,以一个对象取代这些参数。
3.5.8、以异常取代错误码
通过异常捕获统一处理错误,而不是随处直接返回错误码,影响可读性,逻辑完整性。
3.5.9、以测试取代异常
面对调用者可以预先检查的条件,在调用函数之前应该先做检查,而不是直接捕获异常。
3.6、处理概况关系
- 字段上移
- 函数上移
- 构造方法本体上移
- 函数下移
- 字段下移
- 提炼子类
- 提炼超类
- 塑造模板方法
3.7、大型重构
在一知半解的情况下做出的设计决策,一旦堆积起来,也会使你的程序陷于瘫痪。通过重构,可以保证随时在程序中反映出完整的设计思路。
- 梳理并分解继承体系
- 将过程化设计转化为对象设计
- 将领域和表述/显示分离
- 提炼继承体系