文章目录
- 前言
- 重构是什么?
- 常用重构手法
- 提炼函数(Extract Method)
- 以查询取代临时元素(Replace Temp with Query)
- 分解条件表达式(Decompose Conditional)
- 提炼类(Extract class)
- 提炼接口(Extract Interface)
- 以函数取代参数(Replace Parameter with Methods)
- 保持对象完整(Preserve Whole Object)
- 隐藏“委托关系”(Hide Delegate)
- 内联函数(Inline Method)
- 引入外加函数(Introduce Foreign Method)
- 引入本地扩展(Introduce Locale Extension)
- 以委托取代继承(Replace Inheritance with Deegation)
- 代码中的“坏味道”
- 重复代码:提炼函数;以查询取代临时元素。
- 过长函数:分解条件表达式
- 过长的类:提炼类、提炼接口
- 过长参数列表:以函数取代参数;传值改为传对象;组装对象
- 发散式变化:提炼类、细粒度化
- 霰弹式修改:收敛类
- 依恋情结:消除分散,加强封装
- 数据泥团:抽出新对象
- 基本类型偏执:以对象取代基本类型
- switch过多:改为多态
- 平行继承体系:消除重复、消灭类似父类
- 冗赘类:折叠继承体系、内联化类
- 夸夸其谈未来性:警惕代码过度/提前设计
- 令人迷惑的临时变量:提炼类、减少临时变量使用
- 过度耦合消息链:隐藏“委托关系”、减少链式调用
- 中间人:移除中间人、内联函数
- 狎昵关系:改双向关联为单向关联,提炼类
- 异曲同工的类:重命名、搬移函数、提炼超类
- 不完美的库类:引入外加函数、引入本地扩展
- 纯稚数据类:封装字段;移除设值函数;隐藏函数。
- 被拒绝的馈赠:字段/方法下移;以委托取代继承
- 过多的注释:记录将要干什么、无十足把握代码
前言
本文是笔者毕业后的第一篇blog,将从三个方面讨论代码重构。即:1.代码重构是什么;2.常用的重构手法;3.代码中的“坏味道”。本文的姊妹篇是程序员必学的代码重构(实战篇)。
本篇blog是《重构,改善代码既有代码的设计》(密码: ab5g)一文的读书笔记,读书笔记与书一起食用效果更佳哦。欢迎点赞、收藏、评论三连~,谢谢大家。
重构是什么?
谈谈定义
重构是软件内部结构的一种调整,在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。敲重点:1. 使软件更容易被理解和修改;2. 不改变软件外部行为,对外部用户/程序员不感知。
何时重构?
满足以下原则考虑重构:1. 三次原则:“事不过三,三则重构”(反感此处代码的修改,容忍不超过三次);2. 添加功能时重构;3. 修补bug时重构;4. Review代码时重构。
常用重构手法
常用的重构手法将以总结、具体做法、代码示例三个步骤来进行说明。
提炼函数(Extract Method)
将该段代码放进一个独立函数中,并让函数名称解释该函数的用途。过长函数需要注释才能让人理解,此时应抽出独立函数,使之易复用;易读;易复写。
具体做法:
1. 无局部变量:简单复制、粘贴到被提炼函数即可;
2. 有局部变量但是被提炼代码段只读取该变量,不修改:传参给被提炼函数即可;
3. 有局部变量且再赋值:
3.1. 该变量只在被提炼代码段使用:将临时变量声明一并移入被提炼代码段,一起提炼出去;
3.2. 该变量在被提炼代码段之外也有使用:让提炼函数返回该变量改变后的值。有人会问:如果返回变量不止一个,怎么办?安排多函数返回多个值or挑选另一块代码来提炼,每个函数只返回一个值。使用Replace Temp with Query减少临时变量。
代码示例:
public class ExtractMethod {
/**
* 重构前
*/
void printOwing(double amount) {
printBanner();
//print Details
System.out.println("name" + name);
System.out.println("amount" + amount);
}
/**
* 重构后
*/
void printOwingRefactor(double amount) {
printBanner();
printDetails(amount);
}
private void printDetails(double amount) {
System.out.println("name" + name);
System.out.println("amount" + amount);
}
}
以查询取代临时元素(Replace Temp with Query)
将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用,那么新函数可被其他函数使用。
具体做法:
1. 找出被赋值一次的临时变量(如果临时变量被赋值超过多次,使用Split Temporary Variable将其分割为多个变量);
2. 将临时变量修改成final;
3. 将“临时变量赋值”等号右侧部分提炼到一个独立函数中 ;
4. 最后将变量替换为方法,去掉final语句。
代码示例:
public class ReplaceTempWithQuery {
/**
* 重构前
* @return
*/
double printOwing() {
double basePrice = quality * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
}
/**
* 重构后
* @return
*/
double printOwingRefactor() {
if (basePrice() > 1000) {
return basePrice() * 0.95;
} else {
return basePrice() * 0.98;
}
}
double basePrice() {
return quality * itemPrice;
}
}
分解条件表达式(Decompose Conditional)
复杂条件逻辑容易导致复杂度上去,大型函数会使代码可读性降低。可从if,then,else三个中分别提炼出独立函数。
代码示例:
public class DecomposeConditional {
/**
* 重构前
*/
void refactorBefore() {
if (data.before(SUMMER_START) || data.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
} else {
charge = quantity * summerRate;
}
}
/**
* 重构后
*/
void refactorAfter() {
if (notSummer(data)) {
charge = winterCharge(quantity);
} else {
charge = summerCharge(quantity);
}
}
....
}
提炼类(Extract class)
某类做了两个类应该做的事,建立一个新类,将相关的字段和函数从旧类搬移至新类。
具体做法:
1. 新建类,从新类中分离旧类责任,建立“从旧类访问新类”(尽量单向,否则依赖接口)连接关系;
2. 逐步移动字段(Move Field);
3. Move Method;精简类接口,判断是否需要公开此类。
代码示例:
/**
* 重构前
*/
class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getOfficeAreaCode() {
return officeAreaCode;
}
public void setOfficeAreaCode(String officeAreaCode) {
this.officeAreaCode = officeAreaCode;
}
public String getTelephoneNumber() {
return "(" + officeAreaCode + ")" + officeNumber;
}
public void setOfficeNumber(String officeNumber) {
this.officeNumber = officeNumber;
}
}
/**
* 重构后
*/
class PersonRefactor {
//2.建立从Person到TelePhoneNumber的连接
TelePhoneNumber telePhoneNumber = new TelePhoneNumber();
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
TelePhoneNumber getOfficeTelephone() {
return telePhoneNumber;
}
public String getTelephoneNumber() {
return telePhoneNumber.getTelephoneNumber();
}
}
//1.将电话号相关行为分离至独立类中
class TelePhoneNumber {
//3.Move Field移动一个字段
private String areaCode;
private String number;
//4.Move Method将相关函数移动至TelePhoneNumber类中
public String getAreaCode() {
return areaCode;
}
public void setAreaCode(String areaCode) {
this.areaCode = areaCode;
}
public void setNumber(String number) {
this.number = number;
}
public String getNumber() {
return number;
}
public String getTelephoneNumber() {
return "(" + areaCode + ")" + number;
}
}
提炼接口(Extract Interface)
若干用户使用类接口中的同一子集,或者两个类的接口中有部分相同,那么将相同的子集提炼至一个独立接口中。
具体做法:
1. Extract interface只能提炼共通接口,不能提炼共通代码。新建空接口;
2. 在接口中声明待提炼类的共通操作;让相关类实现上述接口;
3. 调整客户类型声明,令其使用该接口。
补充:多态满足的三个条件:继承;重写;父类引用指向子类对象:Parent p = new Child(); 实现多态的三种方法:重写、接口、抽象类和抽象方法。
代码示例:
public class ExtractInterface {
/**
* 重构前
*/
double charge(Employee emp, int days) {
int base = emp.getRate() * days;
if (emp.hasSpecialSkill()) {
return base * 1.05;
} else {
return base;
}
}
/**
* 重构后;3.调整客户端声明,令其使用该接口
*/
double charge(Billable emp, int days) {
int base = emp.getRate() * days;
if (emp.hasSpecialSkill()) {
return base * 1.05;
} else {
return base;
}
}
}
/**
* 1.新建空接口,在接口声明待提炼类的共通操作
*/
interface Billable {
//员工级别
public int getRate();
//是否有特殊技能
public boolean hasSpecialSkill();
}
/**
* 2.让Employee实现上述接口
*/
class Employee implements Billable {
//....
}
以函数取代参数(Replace Parameter with Methods)
将参数计算过程提炼至独立函数中;本体内引用该函数的地方改为调用新建的函数;替换完后,修改并测试;全部替换后,移除参数。
代码示例:
public class ReplaceParameterWithMethods {
/**
* 重构前
*/
void beforeRefactor() {
//...
int basePrice = _quantity * _itemPrice;
discountLevel = getDiscountLevel();
double finalPrice = discountedPrice(basePrice, discountLevel);
}
/**
* 重构后
*/
void beforeRefactor() {
//...
int basePrice = _quantity * _itemPrice;
double finalPrice = discountedPrice(basePrice);
}
private double discountedPrice(int basePrice) {
discountLevel = getDiscountLevel();
}
}
保持对象完整(Preserve Whole Object)
假设你从某对象取出若干值,将它们作为某一次调用时的参数。不如改为传递整个对象。
代码示例:
public class PreserveWholeObject {
/**
* 重构前
*/
int low = daysTempRange().getLow();
int high = daysTempRange().getHigh();
withinPlan = plan.withinRange(low, high);
/**
* 重构后
*/
withinPlan = plan.withinRange(daysTempRange());
}
隐藏“委托关系”(Hide Delegate)
客户通过一个委托类来调用另一个对象,在服务类上建立客户所需要的所有函数,用以隐藏委托关系。
代码示例:
public class HideDelegate {
/**
* 重构前
*/
//此时调用链为 manager=john.getDepartment().getManager();
class Person {
Department department;
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
}
class Department {
private String chargeCode;
private Person manager;
public Department(Person manager) {
this.manager = manager;
}
public Person getManager() {
return manager;
}
//...
}
/**
* 重构后
*/
//2.调整用户,令它只调用服务对象提供的函数
// manager=john.getManager();
class PersonRefactor {
Department department;
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
//1.对于每一个委托关系中的函数,在服务对象端建立一个简单的委托函数
public Person getManager() {
return department.getManager();
}
}
}
内联函数(Inline Method)
一个函数的本体与名称同样清楚易懂,在函数调用点插入函数本体,然后移除该函数。
代码示例:
/**
* 重构前
*/
int getRating() {
return moreThanFiveLateDeliveries() ? 2 : 1;
}
private boolean moreThanFiveLateDeliveries() {
return numberOfLateDeliveries > 5;
}
/**
* 重构后
*/
int getRatingRefactor() {
return (numberOfLateDeliveries > 5) ? 2 : 1;
}
引入外加函数(Introduce Foreign Method)
你需要为提供的类增加一个函数,但你无法修改这个类。在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。
代码示例:
/**
* 代码重构前
*/
public void beforeRefactor() {
Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
}
/**
* 代码重构后
*/
public void afterRefactor() {
Date newStart = nextDay(previousEnd);
}
private static Date nextDay(Date previousEnd) {
return new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
}
引入本地扩展(Introduce Locale Extension)
你需要为服务类额外提供一些函数,但你无法修改这个类:建立新类,使它包含这些额外函数。让这个扩展品成为源类的子类或包装类。
代码示例:
//1.建立扩展类,将它作为原始类的子类
class MfDataSub extends Date {
public MfDataSub(String dataString) {
super(dataString);
}
//2.在扩展类中加入转型构造函数
public MfDataSub(Date arg) {
super(arg.getTime());
}
//3.添加新特性,Move Method将所有外加函数搬移至扩展类
Date nextDay() {
return new Date(getYear(), getMonth(), getDate() + 1);
}
}
以委托取代继承(Replace Inheritance with Deegation)
某子类只使用超类接口中的一部分,或是根本不需要继承而来的数据,在子类中新建一个字段用于保存超类,调整子类函数,令它改成委托超类,去掉两者间继承关系。
代码示例:
/**
* 重构前
*/
class MyStack extends Vector {
public void push (Object element) {
insertElementAt(element, 0);
}
public Object pop() {
Object result = firstElement();
removeElementAt(0);
return result;
}
}
/**
* 重构后
*/
//3.去除两个类之间的继承关系,新建受拖累对象赋给受托字段
class MyStackRefactor {
//1.在子类中新建一字段,引用超类实例,将其初始化为this
private Vector vector = new Vector();
//2.修改子类中的所有函数,让其不再使用超类,转而使用上述的委托字段
public void push (Object element) {
vector.insertElementAt(element, 0);
}
public Object pop() {
Object result = vector.firstElement();
vector.removeElementAt(0);
return result;
}
//4.针对客户端所用的每一个超类函数,为它添加一个简单的委托函数。
public int size() {
return vector.size();
}
public boolean isEmpty() {
return vector.isEmpty();
}
}
代码中的“坏味道”
本节主要讲什么是“坏味道”。嗅到代码中的“坏味道”是在培养自己的判断力,即判断一个类中有多少实例变量算是太大,一个函数有多少行代码才算太长等。本节中的小标题格式为:代码中的坏味道+解决方案。
重复代码:提炼函数;以查询取代临时元素。
假设你在一个以上的地点看到相同的程序结构,设法将它们合二为一。步骤如下:
1. 最简单的重复代码是利用Extract method提炼重复代码;
2. 互为兄弟的子类含相同表达式,两个类先使用Pull up method将其推入超类中,若相似而非相同,应当使用Extract method将相似/差异部分隔开。
3. 如果两个无关的类出现重复代码,那么使用Extract class将重复代码提炼到一个独立类中,在另一个类中调用新类,且需判断放在哪里最合适。
过长函数:分解条件表达式
程序越长越难以理解。小函数易理解的关键是有好名字,他人仅通过名字便可了解函数作用。步骤如下:
1.代码需要注释说明点什么,抽出独立函数并以其用途而非实现手法命名;
2.条件表达式和循环也是提炼信号,条件表达式可使用Decompose Conditional处理表达式。
3.运用Extract Method将许多参数和临时变量当做参数传递给被提炼出来的函数;
4.运用Replace Temp With Query来消除临时元素。
过长的类:提炼类、提炼接口
过长的类会导致代码重复、混乱。Extracrt class将几个变量提炼至新类,选择彼此相关的变量;若提炼适合作为一个子类,Extract subClass往往简单。有个小技巧:使用Extract Interface为每一种使用方式提炼出一个接口,可以帮你分解此类。
过长参数列表:以函数取代参数;传值改为传对象;组装对象
太长参数会导致前后不一致、难以理解、不易使用;全局对象有很多弊端;对象可以有效地解决这一切。
1. 如果向已有对象发出一条请求可以取代一个参数,那么应该使用Replace Parameter with Methods;
2. 可以使用Preserve Whole Object来将同一对象一堆数据收集起来;
3. 某些数据缺乏合理归属,使用Introduce Parameter Object(引入参数对象)为他们制造一个参数对象。
发散式变化:提炼类、细粒度化
某个类因不同的原因在不同的方向上发生变化,针对外界变化的所有相应修改,都只应发生在单一类中,找出某特定原因造成的所有变化。使用Extract Class将它们提炼至另一个类(有可能是子类)中。(拆类、细粒度化)
霰弹式修改:收敛类
每遇到变化,你得在许多不同的类中做出许多小修改,不累么?步骤如下:
使用Move Method(当两类之间太多耦合,将某个类中的方法移动至另一类中)和Move Field(在目标类中新建一个字段,修改源字段的所有用户,令它们改用新字段)将需要修改的代码放在同一类中,若没有,就创造一个,将一系列行为放进同一类中。(收敛类,将一系列相关行为放入同一类中)
依恋情结:消除分散,加强封装
函数对别的类的调用超过对自己所处类的调用。Extract Method 和Move Method用起来。(数据+操作=封装)
假设某函数会用到几个类的功能。判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据放在一起。保持变化只在一处发生。(消除分散+加强封装)
数据泥团:抽出新对象
很多地方都会看到相同的三四项数据:两个类中的相同字段、函数签名中的相同参数等等。解决方案如下:
1. 针对相同字段,运用Extract Class将它们提炼至独立对象;
2. 针对签名中的相同参数,运用Introduce ParameterObject(引入参数对象:以一个对象取代这些参数,因某些参数总是很自然的出现)或Preserve Whole Object(保持对象完整)。
3. 不必在意只用到新对象的一部分,只要新对象取代两个/更多的字段,就可行。
基本类型偏执:以对象取代基本类型
数据有两种:结构型(String,Data)和基本类型(int,double)。尽量以对象取代基本类型:
1.使用Replace Data Value with Object(以对象取代数据值)将本单独存在的数据值替换为对象。
2.使用Replace Array With Object(对象取代数组,对于数据组中的每个元素,以一个字段来表表示)。
3.总之:灵活使用、抽出总在一起的数据并封装对象。
switch过多:改为多态
面向对象即少用switch,switch意味着重复,使用多态解决。
1. Extract Method将switch提炼到一个独立函数中,
2. Move Method将其搬到需要多态性的类中。此时,你可使用Replace Type Code With SubClass(以子类取代类型码:针对一个不可变的类型码,它会影响类的行为)或Replace Type Code With state/strategy(以状态对象取代类型码:针对类型码,它会影响类的行为,且无法通过继承来消除)
3. 如果只在单一函数中有些选择示例,且不想改动它们,那么Replace Parameter with Explicit Methods(以明确函数取代参数:针对参数的每一个可能值,建立独立函数。针对函数取决于参数值而选择不同行为)是个不错的选择。
平行继承体系:消除重复、消灭类似父类
当你为某类新加一个子类,那么必须要在另一个类中也要新加子类。消除重复性的步骤是:
让一个继承体系的实例引用另一个继承体系的实例,在使用Move Method和Move Field将引用端继承体系消灭掉。
冗赘类:折叠继承体系、内联化类
1. 针对子类未做足够工作的情况,使用Collapse Hierarchy(折叠继承体系:超类和子类之间并无太大区别,将它们合为一体)来做足够工作;
2. 针对几乎没用的组件,可以使用Inline class(将类内联化:某个类没做太多事情,将这个类的所有特性搬移至另一个类中,然后移除原类)来处理。
夸夸其谈未来性:警惕代码过度/提前设计
过度设计会导致系统更难理解和维护。
1. 假设某抽象类没啥用,使用Collapse Hierarchy(折叠继承体系);
2. 假设函数无必要,使用Inline class(将类内联化);
3. 假设参数未用上,使用Remove Parameter(移出参数:函数本体无需某参数,将该参数去掉);
4. 假设函数命名有点扯,使用Rename Method(函数改名:函数名称未能揭示函数的用途,修改函数名称)。
令人迷惑的临时变量:提炼类、减少临时变量使用
看到一个未使用过的临时变量,会让人疯的。
假设类中有复杂算法,牵扯到好几个变量,实现者不希望传一堆参数,那么Extract Class将这些变量和相关函数提炼到一个独立类中,提炼后的新对象将是一个函数对象。
过度耦合消息链:隐藏“委托关系”、减少链式调用
一对象请求另一对象,再往后请求另一个…。消息链的产生让代码结构耦合,这时应当使用Hide Delegate(隐藏“委托关系”)。看看消息链最终得到的对象是干什么,Extract Method提炼独立函数,Move Method推入消息链。
中间人:移除中间人、内联函数
对象的封装特点是对外隐藏细节。封装伴随着委托,也要防止过度委托。
1. 某类接口超过半数函数都委托给其他类是不合适的,使用Remove Middle Man(移除中间人:某个类做了过多的简单委托动作,让客户直接调用委托类,与Hide Delegate恰好相反)。
2. 函数“不干实事”,可以使用Inline Method(内联函数:一个函数的本体与名称同样清晰易懂,在函数调用点插入函数本体、然后移除该函数)
狎昵关系:改双向关联为单向关联,提炼类
恋人之间花费较多时间探究彼此private的东西无可厚非,但过分狎昵的类必须拆散。
1. 使用Move Method和Move Field划清界限;
2. 尝试运用Change Bidirectional Association to Undirectional(将双向关联改为单向关联:两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。因此去除不必要的关联);
3. 如果两个类实在情投意合,那么Extract Class将共同点提炼至安全地点,
4. 或者使用HideDeletegate隐藏“委托关系”。甚至可以去掉子类与超类的继承关系。
异曲同工的类:重命名、搬移函数、提炼超类
1. 如果两个函数做同一件事却拥有不同的名称,使用Rename Method(函数重命名);
2. 如果不够,可以使用Move Method(搬移函数:将某些行为移入类,直到两者协议一致)
3. 如果代码仍然有冗余,那么Extract SuperClass(提炼超类:两个类有相似特性,为两个类建立超类,将相同特性移至超类)。
不完美的库类:引入外加函数、引入本地扩展
类库不够完美,在库上做一层封装。使用Introduce Foreign Method(引入外加函数)或Introduce Locale Extension(引入本地扩展)。
纯稚数据类:封装字段;移除设值函数;隐藏函数。
1. 数据类只有字段和访问字段的函数,使用Encapsulate Field(封装字段:你的类中存在一个public字段,将它声明为private,并提供相应的访问函数);
2. 如果有集合,那么使用Encapsulate Collection(封装集合:有的返回函数有集合,让这个函数返回该集合的一个只读副本;并提供添加/移除集合元素的函数);
3. 如果类中有某些字段不想被修改,那么使用Remove Setting Method(移除设置函数:类中的某个字段应该在对象创建时被设值,然后就不再改变,去掉该字段的设值函数,将该字段声明为final)。
4. 如果函数没啥用,可以使用Hide Method(隐藏函数:当一函数未被任何类调用或者提供过多行为的接口时,就必须将该函数声明为private)
被拒绝的馈赠:字段/方法下移;以委托取代继承
1. 所有超类都应该是抽象的。子类应当继承自超类的函数和数据,
2. 但如果子类不想继承,新建子类的兄弟类,使用Push Down Method(方法下移:超类中的某函数只与部分子类相关,将这个函数移到相关子类中去)和Push Down Field(字段下移:超类中的某字段只与部分子类相关,将这个字段移到相关子类中去)。
3. 如果不想修改继承体系,那么使用Replace Inheritance with Delegation(以委托取代继承)来达到目的。
过多的注释:记录将要干什么、无十足把握代码
当你感觉需要撰写注释时,可以尝试重构,试着让注释变得多余。如果你需要记录将要干什么或者对这部分代码无十足把握(自己“为什么做某事”),这时需要写注释。