近期温习了重构经典著作《重构-改善既有代码的设计》,还是要把看书的一些理解和问题记录和分享出来。并且是一个持续跟进优化的过程。陆续把书中讲述的重构方法列举一下。
重构的很大一部分工作就是拆解过长函数(Long Method)。重新组织函数部分,书中介绍的有以下常见方法:
- 提炼方法(Extract Method)
有一段代码可以被组织在一起并独立出来,将这段代码放在一个独立的方法中,并让方法名称解释该方法的用途。
1. 动机:
当方法过长或者一个方法中可以分开小的部分,每个部分都需要一段注释介绍来让人理解代码的用途就需要考虑把方法进行拆解,拆分的粒度的话可以按照每段注释来拆解方法。这样做的好处:1. 如果方法粒度比较小,方法被复用的机会就更大;2. 让高层函数读起来就像一系列注释(更容易读);3. 方法的覆写(override)变得更容易;4. 如果有单元测试的话,测试用例也更方便覆盖。
2. 做法以及注意点:
首先,每创造一个新方法,务必要起一个合适的名字(以它“做什么”来命名,而不是“怎样做”来命名)。关于如何恰如其分的为方法命名,后续也专门整理提供一片博文。
3. 代表性示例:
一般遇到需要使用 提炼函数(Extract Method)方法的场景有:“无局部变量”;“有局部变量”;“对局部变量再赋值” 三种情况,相比之下而“对局部变量再赋值”最为复杂,所以我这里直接给出“对局部变量再赋值”的示例
1. 第一种情况:
void printOwing() {
Enumeration enumeration = orders.elements();
double outstanding = 0.0;
//printBanner
System.out.println("***************");
System.out.println("*** Customer Owes ***");
System.out.println("***************");
//calculate outstanding
while (enumeration.hasMoreElements()) {
Order each = (Order) enumeration.nextElement();
outstanding += each.getAmount();
}
//print details
System.out.println("name" + name);
System.out.println("outstanding" + outstanding);
}
这个时候,就可以把上面带有注释的部分提成方法
void printOwing() {
printBanner();
double outStanding = getOutStanding();
printDetails(outStanding);
}
//无局部变量
void printBanner() {
System.out.println("***************");
System.out.println("*** Customer Owes ***");
System.out.println("***************");
}
//有局部变量
void printDetails(double outstanding) {
System.out.println("name" + _name);
System.out.println("outstanding" + outstanding);
}
//对局部变量再赋值
double getOutStanding() {
//calculate outstanding
Enumeration enumeration = _orders.elements();
double outstanding = 0.0;
while (enumeration.hasMoreElements()) {
Order each = (Order) enumeration.nextElement();
outstanding += each.getAmount();
}
}
2. 第二种情况:
在1的示例中,outstanding变量只是单纯被初始化为一个明确的值。没有做任何处理,所以就可以放在提炼的方法中进行初始化,如果除了初始化之后,还需要做其它处理,那么就需要将其作为一个入参,传到提炼出来的方法中,如下:
void printOwing(double previousAmount) {
Enumeration enumeration = _orders.elements();
double outstanding = previousAmount * 1.2;
printBaner();
//calculate outstanding
while (enumeration.hasMoreElements()) {
Order each = (Order) enumeration.nextElement();
outstanding += each.getAmount();
}
printDetails(outstanding);
}
提炼函数(Extract Method)后的结果:
void printOwing(double previousAmount) {
double outstanding = previousAmount * 1.2;
printBanner();
outstanding = getOutstanding(outstanding);
printDetails(outstanding);
}
double getOutstanding(double initialValue) {
double result = initialValue;
Enumeration enumeration = _orders.elements();
while (enumeration.hasMoreElements()) {
Order each = (Order) enumeration.nextElement();
result += each.getAmount();
}
return result;
}
这样,编译测试通过之后,还可以把 outstanding 的初始化过程整理一下:
void printOwing(double previousAmount) {
printBanner();
outstanding = getOutstanding(previousAmount * 1.2);
printDetails(outstanding);
}
- 内联函数(Inline Method)
一个方法的本体与名称同样清楚易懂。在方法调用点插入方法体,然后移除该方法
1. 动机:
当发现一个方法内有很多不合理的方法,这个时候,就可以把这些方法都内联回调用方法的这个大方法中,重新再对方法进行提炼抽取的时候使用。
2. 代表性示例:
int getRating() {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() {
return _numberOfLateDeliveries > 5;
}
重构为
int getRating() {
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}
- 内联临时变量(Inline Temp)
有一个临时变量,只被简单表达式赋值一次,而它妨碍了其它重构方法,将所有对该变量的引用,替换为对他赋值的那个表达式自身。
1. 动机:
当你发现某个临时变量被赋予某个函数调用的返回值。这样的临时变量不会有任何危害,但是如果这个临时变量妨碍了其它的重构方法,这个时候,就需要将其内联化。
2. 做法以及注意点:
首先确保临时变量只是被赋值了一次,然后其它地方都仅仅是在引用而没有再对其赋值,这个时候可以将其内联
首先如果要内联的变量没有被声明为 final 类型,就先把他声明为 final 类型,然后编译。这样可以检查这个临时变量是否真的只被赋值一次。如果确实只被赋值一次,那就把所有的引用点,替换为“为临时变量赋值”的表达式。
- 已查询替代临时变量(Replace Temp with Query)
程序中以一个临时变量保存某个表达式的运算结果,将这个表达式提炼到一个独立方法中,将这个临时变量的所有引用点替换为对新方法的调用。此后,新方法可以被其它方法调用。
1. 动机:
临时变量的问题在于:他们是暂时的,并且只在所属的方法内可见,而且只能在所属方法中调用,所以一个类中的多个方法都需要用到某些临时变量的时候,如果把临时变量替换为一个查询,那么同一个类中的所有方法都可以获取这份信息。
局部变量会让代码难以被提炼方法,所以应该尽可能把它们替换为查询式。
2. 做法以及注意点:
一般情况下临时变量只被赋值一次,但是如果临时变量是用来收集结果的(比如循环中的累加值),就需要将某些程序逻辑(比如循环)也复制到查询方法中去。
3. 代表性示例:
double getPrice() {
int basePrice = _quantity * _itemPrice;
double discountFactor;
if(basePrice > 1000) {
discountFactor = 0.95;
} else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}
我们希望把两个临时变量都替换掉,结果如下:
//首先,先把它们设置成final类型,进行编译,确保它们只是被简单赋值一次
double getPrice() {
final int basePrice = _quantity * _itemPrice;
final double discountFactor;
if(basePrice > 1000) {
discountFactor = 0.95;
} else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}
//上面操作完之后,编译没有问题的话,接下来提炼赋值动作右侧的表达式
double getPrice() {
final int basePrice = getBasePrice();
final double discountFactor;
if(basePrice > 1000) {
discountFactor = 0.95;
} else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}
private int getBasePrice() {
return _quantity * _itemPrice;
}
接下来,使用 Inline Temp 来进一步重构
//basePrice的使用一共有两处,首先替换第一处,然后进行编译测试。
double getPrice() {
final int basePrice = getBasePrice();
final double discountFactor;
if(getBasePrice() > 1000) {
discountFactor = 0.95;
} else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}
private int getBasePrice() {
return _quantity * _itemPrice;
}
//发现没有问题之后,进行第二处替换,并且可以直接把 basePrice 的声明就去掉了。
double getPrice() {
final double discountFactor;
if(getBasePrice() > 1000) {
discountFactor = 0.95;
} else {
discountFactor = 0.98;
}
return getBasePrice() * discountFactor;
}
private int getBasePrice() {
return _quantity * _itemPrice;
}
//然后,通过类似方法重构 discountFactor
double getPrice() {
return getBasePrice() * getDiscountFactor();
}
private int getDiscountFactor() {
if(getBasePrice() > 1000) {
return 0.95;
} else {
return 0.98;
}
}
private int getBasePrice() {
return _quantity * _itemPrice;
}
- 引入解释性变量(Introduce Explaining Variable)
当方法中存在复杂的表达式的时候,将该表达式(或其中的一部分)的结果放进一个临时变量,以变量的名称来解释表达式的用途
1. 动机:
复杂的表达式一般难以阅读,这种情况下,最好使用含义明确的临时变量来替换。适合使用Introduce Explaining Variable的场景:在条件逻辑中,可以一个良好命名的临时变量来解释对应条件子句的意义;在较长的算法中,应用临时变量来解释每一步运算的含义。
2. 做法以及注意点:
依然声明 final 修饰的临时变量,这样可以保证临时变量的单一职责。然后进行替换。
3. 代表性示例:
double price() {
//price is base price - quantity discount + shipping
return _quantity * _itemPrice -
Math.max(0, _quantity - 500) * _itemPrice * 0.05 +
Math.min(_quantity * _itemPrice * 0.1, 100.0);
}
//先把底价=数量*单价提取到临时变量中,并且有两处使用的地方,逐一替换
double price() {
final double basePrice = _quantity * _itemPrice;
//price is base price - quantity discount + shipping
return basePrice -
Math.max(0, _quantity - 500) * _itemPrice * 0.05 +
Math.min(basePrice * 0.1, 100.0);
}
//将 quantity discount 再提取出来
double price() {
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.max(0, _quantity - 500) * _itemPrice * 0.05;
//price is base price - quantity discount + shipping
return basePrice -
quantityDiscount +
Math.min(basePrice * 0.1, 100.0);
}
//最后再把运费 shipping 提取出来
double price() {
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.max(0, _quantity - 500) * _itemPrice * 0.05;
final double shipping = Math.min(basePrice * 0.1, 100.0);
//price is base price - quantity discount + shipping
return basePrice -
quantityDiscount +
shipping;
}
然后,通常不以变量来解释动作含义,我们再通过 Extend Method 进一步重构
double price() {
//price is base price - quantity discount + shipping
return getBasePrice() -
getQuantityDiscount() +
getShipping();
}
private double getBasePrice() {
return _quantity * _itemPrice;
}
private double getQuantityDiscount() {
return Math.max(0, _quantity - 500) * _itemPrice * 0.05;
}
private double getShipping() {
return Math.min(basePrice * 0.1, 100.0);
}
这里就自然出现一个问题:那么像上面这种情况下,重构应该止于什么地方呢,作者在书中表述:在进行 Extend Method 需要花费更大的工作量的时候。如果要处理的方法是一个拥有大量局部变量的方法,那么要使用 Extend Method 短期内,变得比较困难,这个时候就到 Introduce Explaining Variable 为止是比较合适的。这里我比较认同作者的观点。
- 分解临时变量(Split Temporary Variable)
当程序中出现临时变量被赋值超过一次,它既不是循环变量,又不是用于收集计算结果。那么就应该针对每次赋值,创造一个独立,对应的临时变量。
1. 动机:
如果一个临时变量被赋值超过一次,就意味着它在方法中承担了一个以上的责任。如果临时变量承担多个责任,就应该分解为多个临时变量,保证每个临时变量只承担一个责任。
2. 做法以及注意点:
通常将新的变量声明为 final。在临时变量第二次赋值动作为界。修改此前对该变量的所有引用,让它们引用新的临时变量。
3. 代表性示例:
//这里是计算一个运动举例。在起点处,静止的物体收到一个初始力的作用而开始运动。一段时间后,第二个力作用于这个五次,让他再次加速。根据牛顿第二定律,计算物体运动的举例
double getDistanceTravelled(int time) {
double result;
double acc = _primaryForce / _mass;
int primaryTime = Math.min(time, _delay);
result = 0.5 * acc * primaryTime;
int secondaryTime = time - _delay;
if(secondaryTime > 0) {
double primaryVel = acc * _delay;
acc = (_primaryForce * secondaryForce) / _mass;
result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
}
return result;
}
重构之后,
double getDistanceTravelled(int time) {
double result;
final double primaryAcc = _primaryForce / _mass;
int primaryTime = Math.min(time, _delay);
result = 0.5 * primaryAcc * primaryTime;
int secondaryTime = time - _delay;
if(secondaryTime > 0) {
double primaryVel = primaryAcc * _delay;
final double secondaryAcc = (_primaryForce * secondaryForce) / _mass;
result += primaryVel * secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime;
}
return result;
}
接下来,还可以继续应用其它重构方法,让方法变得更加易读。
- 移除对参数的赋值(Remove Assignment to Parameters)
对一个参数进行赋值的时候,以一个临时变量取代改参数的位置
1. 动机:
如果直接对方法的入参,进行赋值操作。会降低代码的清晰度,容易让人造成理解偏差而产生误会。
2. 做法以及注意点:
建一个临时变量,把待处理的参数赋值给这个临时变量。
3. 代表性示例:
int discount(int inputVal, int quatity, int yearToDate) {
if(inputVal > 50) {
inputVal -= 2;
}
if(quatity > 100) {
inputVal -= 1;
}
if(yearToDate > 10000) {
inputVal -= 4;
}
return inputVal;
}
/************************************************************/
//重构之后
int discount(int inputVal, int quatity, int yearToDate) {
int result = inputVal;
if(inputVal > 50) {
result -= 2;
}
if(quatity > 100) {
result -= 1;
}
if(yearToDate > 10000) {
result -= 4;
}
return result;
}
这里其实涉及到一个java方法的“按值传递 ”的细节
看下面的例子:
/**
* Created by haoyufei on 18/9/16.
*/
public class Test {
public static void main(String[] args) {
int x = 5;
triple(x);
System.out.println("x after triple:" + x);
}
private static void triple(int arg) {
arg = arg * 2;
System.out.println("x in triple:" + arg);
}
}
结果如下:
第二个例子:
/**
* Created by haoyufei on 18/9/16.
*/
public class Test {
public static void main(String[] args) {
Date date1 = new Date("16 Sep 18");
nextDateUpdate(date1);
System.out.println("date1 after nextDate:" + date1);
Date date2 = new Date("16 Sep 18");
nextDateReplace(date2);
System.out.println("date2 after nextDate:" + date2);
}
private static void nextDateUpdate(Date arg) {
arg.setDate(arg.getDate() + 1);
System.out.println("date1 in nextDate:" + arg);
}
private static void nextDateReplace(Date arg) {
arg = new Date(arg.getYear(), arg.getMonth(), arg.getDay() + 1);
System.out.println("date1 in nextDate:" + arg);
}
}
结果如下:
这两个例子一个是传原生类型,一个是传对象。通过上面的两个例子,可以得出一个结论:在 java 中,对象的引用是按值传递的。所以,可以修改参数对象的内部状态,但对参数对象重新赋值是没有意义的。
- 以方法对象取代方法(Replace Method with Method Object)
有一个大型方法,其中对局部变量的使用导致无法进行 Extend Method 的时候,将这个方法放入一个单独的对象中,这样局部变量就变成了对象内的字段。然后可以在同一个对象中将这个大型的方法分解为多个小方法。
1. 动机:
局部变量会增加方法分解的难度。如果一个方法中局部变量很多,但是又需要重构这个方法,这个时候就应该想到 Replace Method with Method Object 来把局部变量都变成方法对象的字段。
2. 做法以及注意点:
创建一个新类,在新类中创建 final 字段,来保存原先大型方法所在的对象。
同时针对原方法中的每个临时变量和每个参数,在新类中建立一个对应的字段来保存。
在新类中创建一个构造方法,接收源对象及员方法的所有参数作为参数
在新类中创建一个compute()方法来存放原来方法中的代码。如果需要调用源对象的任何方法,请通过源对象字段调用
最后,在旧方法的方法本体替换为“创建上述新类的一个新对象,然后调用其中的compute()方法”
3. 代表性示例:
原先的类
class Account {
int gamma(int inputVal, int quantity, int yearToDate) {
int importantValue1 = (inputVal * quantity) + delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if((yearToDate - importantValue1) > 100) {
importantValue2 -= 20;
}
int importantValue3 = importantValue2 * 7;
return importantValue3 - 2 * importantValue1;
}
}
重构之后的样子
声明一个新类,提供一个final字段用来保存源对象
class Gamma{
private final Account _account;
private int inputVal;
private int quantity;
private int yearToDate;
private int importantValue1;
private int importantValue2;
private int importantValue3;
Gamma(Account source, int inputValArg, int quantityArg, int yearToDateArg) {
_account = source;
inputVal = inputValArg;
quantity = quantityArg;
yearToDate = yearToDateArg;
}
int compute() {
importantValue1 = (inputVal * quantity) + _account.delta();
importantValue2 = (inputVal * yearToDate) + 100;
if((yearToDate - importantValue1) > 100) {
importantValue2 -= 20;
}
int importantValue3 = importantValue2 * 7;
return importantValue3 - 2 * importantValue1;
}
}
//修改旧方法,让它将它的工作委托于刚建立的这个方法对象
int gamma(int inputVal, int quantity, int yearToDate) {
return new Gamma(this, inputVal, quantity, yearToDate).compute();
}
再之后可以进行其它重构,比如讲compute()方法中的某些部分可以提取出来
int compute() {
importantValue1 = (inputVal * quantity) + _account.delta();
importantValue2 = (inputVal * yearToDate) + 100;
importantThing();
int importantValue3 = importantValue2 * 7;
return importantValue3 - 2 * importantValue1;
}
void importantThing() {
if((yearToDate - importantValue1) > 100) {
importantValue2 -= 20;
}
}
- 替换算法(Substitute Algorithm)
这种方法比较常见和简单,我就没有再举例子。
注:博文中的例子,很大一部分是直接借鉴的 经典著作《重构-改善既有代码的设计》中的例子。
我自己的理解:书中描述的这些方法仅仅是一些借鉴的经验和手段,也是一些常规做法,至于重构的时候,方法的粒度往往需要自己在实践的过程中根据实际情况来确定,并没有一个放之四海而皆准的通用法则,并且根据个人的视角不同结果也会不同,就像那句话“一千个人心中有一千个哈姆雷特”一样。
并且进行重构的时候,每一步的跨度大小也是因人而异,熟练的高手可能在看到某种需要重构的代码的时候,一下就能够跳到最后一步。刚开始进行重构的话,还是需要小步快跑,多编译运行,多测试,要保证质量。
需要做的事情:
- 整理方法命名规则以及常见词汇查找文档或者规范,结合阿里的规范