文章目录
重构函数
Extract Method 提炼函数
你有一段代码可以被组织在一起并独立出来
将这段代码放进一个独立函数中,并让函数名解释该函数的用途
void printOwing(double amount) {
printBanner();
// print details
System.out.println("name:" + this.name);
System.out.println("amount:" + amount);
}
提炼函数之后:
void printOwing(double amount) {
printBanner();
printDetail(amount);
}
private void printDetails(double amount) {
System.out.println("name:" + this.name);
System.out.println("amount:" + amount);
}
动机:
当看到一个过长的函数或者一段需要注释才能让人理解用途的代码,可使用Extract Method将这段代码放进一个独立函数中
做法:
1. 创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”命名)
2. 将提炼出来的代码从源函数复制到新建的目标函数中
3. 检查提炼出的代码,查看是否引用了"作用域限于源函数"的变量(包括局部变量和源函数参数)
4. 检查是否有“仅用于被提炼代码段”的临时变量。若有,在目标函数中将它们声明为临时变量
5. 检查被提炼代码段,查看是否有任何局部变量的值被它改变。若一个临时变量值被修改了,查看是否可将提炼代码段处理为一个查询,并将结果赋值给相关变量。若很难做到,或被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。你可能需要先使用Split Temporary Variable,然后再尝试提炼。也可以使用Replace Temp with Query将临时变量消灭
6. 将被提炼代码段中需要读取的局部变量,当做参数传给目标函数
7. 在源函数中,将被提炼代码段替换为对目标函数的调用(若将任务临时变量移到目标函数中,检查它们原来的声明式是否在被提炼代码段的外围,若是,则删除声明式)
范例:
源函数
void printOwing() {
Enumeration e = this.orders.elements();
double outstanding = 0.0;
// print banner 1
System.out.println("------------");
// calculate outstanding 2
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
// print details 3
System.out.println("name:" + this.name);
System.out.println("amount:" + outstanding);
}
banner,calculate,details分别是三种类型的提炼函数
无局部变量:
// print banner 1
System.out.println("------------");
==>
printBanner();
void printBanner() {
// print banner
System.out.println("------------");
}
Extract Method的困难点在于局部变量,包括传进源函数的参数和源函数所声明的临时变量。局部变量的作用域仅限于源函数,所以当使用Extract Method时必须花费额外功夫处理这些变量
局部变量最简单的情况是:被提炼代码段只是读取这些变量的值,并不修改它们。这种情况可以简单地将它们当作参数传给目标函数
有局部变量:
// print details 3
System.out.println("name:" + this.name);
System.out.println("amount:" + outstanding);
==>
printDetails(outstanding);
private void printDetails(double outstanding) {
System.out.println("name:" + this.name);
System.out.println("amount:" + outstanding);
}
必要时,可使用这种手法处理多个局部变量
若局部变量是个对象,而被提炼代码段调用了会对该对象造成修改的函数,也可如法炮制。同样只需将这个对象作为参数传递给目标函数即可。只有在被提炼代码段真的对一个局部变量赋值的情况下,才必须采取其他措施
若被提炼代码段对局部变量赋值,问题变得更复杂。此处讨论临时变量的问题:若你发现源函数的参数被赋值,应使用Remove Assignments to Parameters
被赋值的临时变量分两种情况。
- 较简单的情况:这个变量只在被提炼代码段中使用。此时可将这个临时变量的声明移到被提炼代码段中,然后一起提炼出去。
- 另一种情况是:被提炼代码段之外的代码也使用了这个变量
: 1.若这个这个变量在被提炼代码段之后未再被使用,则只需直接在目标函数中修改它即可;
2.若被提炼代码段之后的代码还使用了这个变量,你就需要让目标函数返回该变量改变后的值。
对局部变量再赋值:
Enumeration e = this.orders.elements();
double outstanding = 0.0;
// calculate outstanding 2
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
==>
double outstanding = getOutstanding();
double getOutstanding() {
Enumeration e = this.orders.elements();
double result = 0.0;
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
result += each.getAmount();
}
return result;
}
变量e只在提炼代码段中使用,故而将它整个搬到新函数中。double变量outstanding在被提炼代码段内外都被用到,所以必须让提炼出来的新函数返回它
若outstanding有其他处理,则必须将它的值作为参数传给目标函数
void printOwing(double previousAmount) {
Enumeration e = this.orders.elements();
double outstanding = previousAmount * 1.2;
// calculate outstanding 2
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
==>
void printOwing(double previousAmount) {
double outstanding = previousAmount * 1.2;
outstanding = getOutstanding(outstanding);
}
double getOutstanding(double initialValue) {
Enumeration e = this.orders.elements();
double result = initialValue;
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
result += each.getAmount();
}
return result;
}
提炼结果:
void printOwing(double previousAmount) {
printBanner();
double outstanding = getOutstanding(previousAmount * 1.2);
printDetails(outstanding);
}
若返回的变量不止一个:通常挑选另一块代码来提炼,让每个函数都只返回一个值,故而安排多个函数,用以返回多个值
注意:临时变量往往为数众多,甚至会使提炼工作举步维艰。这种情况下,先运用Replace Temp with Query减少临时变量。若即使这么做了依然提炼困难,就动用Replace Method with Method Object
private String name;
private ArrayList orders;
void printOwing(double previousAmount) {
printBanner();
double outstanding = getOutstanding(previousAmount * 1.2);
printDetails(outstanding);
}
void printBanner() {
// print banner
System.out.println("------------");
}
double getOutstanding(double initValue) {
Iterator e = orders.iterator();
double result = initValue;
// calculate outstanding
while (e.hasNext()) {
Order each = (Order) e.next();
result += each.getAmount();
}
return result;
}
void printDetails(double outstanding) {
// print details
System.out.println("name:" + this.name);
System.out.println("amount:" + outstanding);
}
Inline Method 内联函数
一个函数的本体与名称同样清楚易懂
在函数调用点插入函数本体,然后移除该函数
int getRating() {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() {
return _numberOfLateDeliveries > 5;
}
==>
int getRating() {
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}
动机:
有时候遇到某些函数,其内部代码和函数名称同样清晰易读,此时应该去掉该函数,直接使用其中的代码。
你手上有一群组织不甚合理的函数。你可以将它们都内联到一个大型函数中,再从中提炼出组织合理的小型函数。实施Replace Method with Method Object前先Inline Method往往可以获得不错的效果。你可把所要的函数的所有调用对象的函数内容都内联到函数对象中。比起既要移动一个函数,又要移动它所调用的其他所有函数,将整个大型函数作为整体来移动会比较简单
当使用太多间接层,可使用Inline Method
做法:
- 检查函数,确定它不具多态性(若子类继承了这个函数,就不要将次函数内联,因为子类无法覆写一个根本不存在的函数)
- 找出这个函数的所有被调用点
- 将这个函数的所有被调用点都替换为函数本体
- 删除该函数的定义
Inline Temp 内联临时变量
有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法
将所有对该变量的引用动作,替换为对它赋值的那个表达式自身
double basePrice = anOrder.basePrice();
return (basePrice > 1000);
==>
return (anOrder.basePrice() > 1000);
动机:
Inline Temp多半是作为Replace Temp with Query的一部分使用。唯一单独使用Inline Temp的情况是:你发现某个临时变量被赋予某个函数调用的返回值。
**做法:
- 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用
- 若这个临时变量并未被声明为final,那就将它声明为final,然后编译(此处用于检查该临时变量是否真的只被赋值一次)
- 找到该临时变量的所有引用点,将它们替换为"为临时变量赋值"的表达式
- 每次修改后,编译并测试
- 修改完所有引用点后,删除该临时变量的声明和赋值语句
Replace Temp with Query 以查询取代临时变量
你的程序以一个临时变量保存某一表达式的运算结果
将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可被其他函数使用
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
==>
if (basePrice() > 1000) {
return basePrice() * 0.95;
} else {
return basePrice() * 0.98;
}
double basePrice() {
return _quantity * _itemPrice;
}
动机:
临时变量的问题在于:它们是暂时的,且只能在所属函数内使用,由于临时变量只在所属函数内可见,故而会产生更长的函数,只有这样才能访问到需要的临时变量。若把临时变量替换为一个查询,那么同一个类中的所有函数都将可以获得这份信息
Replace Temp with Query往往是运用Extract Method之前必不可少的一个步骤。局部变量会使代码难以被提炼,故而应该尽可能把它们替换为查询式
这个重构手法较为简单地情况是:临时变量只被赋值一次,或者赋值给临时变量的表达式不受其他条件影响。其他情况比较棘手,可能需要先运用Split Temporary Variable 或Separate Query from Modifier使情况变得简单一些,然后再替换临时变量。若你想替换的临时变量是用来收集结果的(如循环中的累加值),就需要将某些程序逻辑(如循环)复制到查询函数去
做法:
简单情况:
- 找出只被赋值一次的临时变量(若某个临时变量被赋值超过一次,考虑使用Split Temporary Variable将它分割成多个变量)
- 将该临时变量声明为final
- 编译(确保该临时变量的确只被赋值一次)
- 将“对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中。
=> 首先将函数声明为private
=> 确保提炼出来的函数无任何副作用,即该函数并不修改任何对象内容。若有副作用,对它使用Separate Query from Modifler - 编译,测试
- 在该临时变量上实施Inline Temp
临时变量有时保存循环中的累加信息。此时整个循环都可以被提炼为一个独立函数;或者针对每个累加值重复一遍循环,如此将所有临时变量都替换为查询
范例:
源函数:
double getPrice() {
int basePrice = _quantity * _itemPrice;
double discountFactor;
if (basePrice > 1000) {
discountFactor = 0.95;
} else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}
先把临时变量声明final:若编译器警告则不该进行这项重构
final int basePrice = _quantity * _itemPrice;
final double discountFactor;
将赋值动作的右侧表达式提炼出来:
double getPrice() {
final int basePrice = basePrice();//此处
final double discountFactor;
if (basePrice > 1000) {
discountFactor = 0.95;
} else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}
int basePrice() {
return _quantity * _itemPrice;
}
运用Inline Temp:替换掉basePrice的引用点
double getPrice() {
final double discountFactor;
if (basePrice() > 1000) { // 此处
discountFactor = 0.95;
} else {
discountFactor = 0.98;
}
return basePrice() * discountFactor; // 此处
}
int basePrice() {
return _quantity * _itemPrice;
}
提炼discountFactor()
double getPrice() {
final double discountFactor = discountFactor(); // 此处
return basePrice() * discountFactor;
}
int basePrice() {
return _quantity * _itemPrice;
}
double discountFactor() {
if (basePrice() > 1000) {
return 0.95;
} else {
return 0.98;
}
}
同样Inline Temp去除引用点:
double getPrice() {
return basePrice() * discountFactor();
}
int basePrice() {
return _quantity * _itemPrice;
}
double discountFactor() {
if (basePrice() > 1000) {
return 0.95;
} else {
return 0.98;
}
}
若没有将临时变量basePrice替换为一个查询式,则难以提炼discountFactor()
Introduce Explaining Variable 引入解释性变量
你有一个复杂的表达式
将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途
if ((platform.toUpperCase().indexOf("MAC") > -1) && (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize > 0)
{}
==>
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
}
动机:
当表达式复杂且难以阅读。此时,临时变量可以帮助将表达式分解为比较容易管理的形式
注意,在**条件逻辑**中,Introduce Explaining Variable特别有价值: 你可以用这项重构将每个条件字句提炼出来,以一个良好命名的临时变量来解释对应条件字句的意义。使用这项重构的另一种情况是,在较长算法中,可以运用临时变量来解释每一步运算的意义
当局部变量使Extract Method难以进行时,可使用Introduce Explaining Variable
做法:
- 声明一个final临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它
- 将表达式中的“运算结果”这一部分,替换为上述临时变量
若被替换的这一部分在代码中重复出现,可每次一个,逐一替换。 - 编译测试
范例:
double price() {
return _quantity * _itemPrice - Math.max(0, _quantity - 500) * _itemPrice * 0.05 + Math.min(_quantity * _itemPrice * 0.01, 100.0);
}
=>>
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.max(0, _quantity - 500) * _itemPrice * 0.05;
final double shipping = math.min(_quantity * _itemPrice * 0.01, 100.0);
==>
return basePrice - quantityDiscount + shipping;
也可以用Extract Method处理以上范例
double price() {
return basePrice() - quantityDiscount() + shipping();
}
private double quantityDiscount() {
return Math.max(0, _quantity - 500) * _itemPrice * 0.05;
}
private double shipping() {
return math.min(_quantity * _itemPrice * 0.01, 100.0);
}
private double basePrice() {
return _quantity * _itemPrice;
}
尽量使用Extract Method分解复杂表达式
注意: Extract Method 需要花费更大工作量时,先使用Introduce Explaining Variable理清代码,然后运用Replace Temp with Query把中间引入的那些解释性临时变量去掉。若最终使用Replace Method with Method Object,那么中间引入的那些解释性临时变量也有其价值
Split Temporary Variable 分解临时变量
你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果
针对每次赋值,创造一个独立,对应的临时变量
double temp = 2 * (_height + _width);
System.out.println(temp);
temp = _height * _width;
System.out.println(temp);
==>
final double perimeter = 2 * (_height + _width);
System.out.println(perimeter);
final double area = _height * _width;
System.out.println(area);
动机:
临时变量有各种不同的用途,其中某些用途会导致临时变量被多次赋值.循环变量(i+=i累加),结果收集变量就是两个例子
除了这两个情况,还有很多临时变量用于保存一段冗长代码的运算结果,以便稍后使用。这种临时变量应该只被赋值一次。若临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,每个变量只承担一个责任。同一个临时变量承担两件不同的事情,会难以阅读
做法:
- 在待分解临时变量的声明及其第一次被赋值处,修改其名称
若稍后之赋值语句时[i=i+…],即结果收集变量,则不要分解它。结果收集变量的作用通常是累加,字符串接合,写入流或者向集合添加元素 - 将新的临时变量声明为final
- 以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量
- 在第二次赋值处,重新声明原先那个临时变量
- 编译,测试,重复上述过程
范例:
起点收到一个初始力的作用加速acc开始运动一段距离result。一段时间secondaryTime后,第二个力作用于物体,让他再次加速acc运动一段距离result
double getDistanceTravelled(int time) {
double result;
// 观察acc
double acc = _primaryForce / _mass;
int primaryTime = Math.min(time, _delay);
result = 0.5 * acc * primaryTime * 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;
}
第一行acc表示一个初始力加速度 第二行acc表示两个力共同造成的加速度
需要分解这两个
==>
final double primaryAcc = _primaryForce / _mass;
第一步分解acc之后
double getDistanceTravelled(int time) {
double result;
// 观察acc
final double primaryAcc = _primaryForce / _mass;
int primaryTime = Math.min(time, _delay);
result = 0.5 * primaryAcc * primaryTime * primaryTime;
int secondaryTime = time - _delay;
if (secondaryTime > 0) {
double primaryVel = primaryAcc * _delay;
// 此处
acc = (_primaryForce + _secondaryForce) / _mass;
result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
}
return result;
}
==>
final double secondaryAcc = (_primaryForce + _secondaryForce) / _mass;
result += primaryVel * secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime;
此时可以通过其他方法消除临时变量
Remove Assignments to Parameters 移除对参数的赋值
代码对一个参数进行赋值
以一个临时变量取代该参数的位置
int discount (int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;
}
==>
int discount (int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;
}
动机:
对参数赋值,当把一个名为foo的对象作为参数传给某个函数,那么"对参数赋值"意味改变foo,使它引用另一个对象。若在被传入对象身上进行什么操作,那没问题,但foo被改而指向另一个对象则不一样
void method(Object foo) {
foo.modifyInSomeWay();
// trouble and despair will follow you
foo = anotherObject;
}
以上代码片段,降低了代码的清晰度,且混用了按值传递和引用传递这两种参数传递方式。java只采用按值传递方式
在java中不要对参数赋值:若你看到手上的代码已经这样做了,请使用Remove Assignments to Parameters
当然面对那些使用出参数的语言,不必遵循这条规则
做法:
- 建立一个临时变量,把待处理的参数值赋予它
- 以"对参数的赋值"为界,将其后所有对此参数的引用点,全部替换为"对此临时变量的引用"
- 修改赋值语句,使其改为对新建之临时变量赋值
范例:
int discount (int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;
if (quantity > 100) inputVal -= 1;
if (yearToDate > 10000) inputVal -= 4;
return inputVal;
}
==> 临时变量取代对参数的赋值动作
int discount (int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;
if (quantity > 100) result -= 1;
if (yearToDate > 10000) result -= 4;
return result;
}
还可以为参数加上关键词final, 强制它遵循"不对参数赋值"
int discount (final int inputVal, final int quantity, final int yearToDate) {
...
}
Replace Method with Method Object 以函数对象取代函数
你有一个大型函数,其中对局部变量的使用使你无法采用Extract Method
将这个函数放进一个单独地对象中,如此一来局部变量就成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数
class Order...
double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// long computation;
...
}
==> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yp6wzM9H-1571747954086)(G:\document\picture\1571737715600.png)]
动机:
只要将相对独立的代码从大型函数中提炼出来,就可以大大提高代码的可读性
但是局部变量的存在会增加函数分解难度。Replace Temp with Query可以减轻这一负担,但有时你会发现根本无法拆解一个需要拆解的函数。此时使用函数对象
Replace Method with Method Object会将所有局部变量都变成函数对象的字段。然后对这个新对象使用Extract Method创造出新函数,从而将原本的大型函数拆解变短
做法:
- 建立一个新类,根据待处理函数的用途,为类命名
- 在新类中建立一个final字段,用以保存原先大型函数所在的对象。我们将这个字段称为"源对象"。同时,针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保存
- 在新类中建立一个构造函数,接收源对象及原函数的所有参数作为参数
- 在新类中建立一个compute函数
- 将原函数的代码复制到compute函数中。若需要调用源对象的任何函数,通过源对象的引用调用
- 编译
- 将旧函数的函数本体替换为这样一条语句:“创建上述新类的一个新对象,而后调用其中的compute函数”
范例:
class Account {
int gamma (int inputVal, int quantity, int yearToDate) {
int importantValue1 = (inputVal * quantity) + delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantValue) > 100) {
importantValue2 -= 20;
}
int importValue3 = importValue2 * 7;
// and so on
return importantValue3 - 2 * importValue1;
}
}
==> 先声明一个新类。在此新类中提供一个final字段用以保存源对象;对于函数的每一个参数和每一个临时变量,也以一个字段逐一保存
class Gamma...
private final Account _account;
private int inputVal;
private int quantity;
private int yearToDate;
private int importantValue1;
private int importantValue2;
private int importValue3;
加入构造函数
Gamma (Account source, int inputValArg, int quantityArg, int yearToDateArg) {
_account = source;
inputVal = inputValArg;
quantity = quantityArg;
yearToDate = yearToDateArg;
}
将原本的函数搬到compute,函数中任何调用Account类的地方,改而使用_account字段
int compute () {
int importantValue1 = (inputVal * quantity) + _account.delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantValue) > 100) {
importantValue2 -= 20;
}
int importValue3 = importValue2 * 7;
// and so on
return importantValue3 - 2 * importValue1;
}
修改旧函数,委托任务
int gamma (int inputVal, int quantity, int yearToDate) {
return new Gamma(this, imputVal, quantity yearToDate).compute();
}
由此带来的好处:轻松对compute采用Extract Method,不必担心参数传递问题
int compute () {
int importantValue1 = (inputVal * quantity) + _account.delta();
int importantValue2 = (inputVal * yearToDate) + 100;
// 此处重构
importantThing();
int importValue3 = importValue2 * 7;
// and so on
return importantValue3 - 2 * importValue1;
}
void importantThing() {
if ((yearToDate - importantValue1) > 100) {
importantValue2 -= 20;
}
}
Substitute Algorithm 替换算法
你想要把某个算法替换为另一个更清晰的算法
将函数本体替换为另一个算法
String foundPerson(String[] people) {
for (int i = 0; i < people.length; i++) {
if (people[i].equals("Don")) {
return "Don";
}
if (people[i].equals("John")) {
return "John";
}
}
return "";
}
==>
String foundPerson(String[] people) {
List candidates = Arrays.asList("Don", "John");
for (int i = 0; i < people.length; i++) {
return people[i];
}
return "";
}
动机:
一般先将复杂的算法分解为较为简单地小型函数,然后再算法替换
做法:
- 准备好另一个算法并通过编译
- 针对现有测试,执行上述新算法。结果一样则结束
- 若结果不一,在调试过程中以旧算法为比较参照标准