在《重构改善既有代码设计》一书中,作者的经验是,重构的大多数手法都是源自对于函数进行的处理,绝大多数是从过长函数开始。结合我的实际处理情况,不外如是。
过长函数,确实很讨厌。因为他们往往向书中所说,会包含太多的信息,这些信息又会被函数错综复杂的逻辑。不易鉴别。如何对付过长函数,作者系统化的总结出了一套经验:
对付过长函数,一项重要的重构手法就是.Extract Method (110), 它把一段代码从原先函 数中提取出来,放进一个单独函数中。而Inline Method(117)正好相反:将一个函数调用动作替换为该函数本体。如果在进行多次提炼之后,意识到提炼所得的某些函数 并没有做任何实质事情,或如果需要回溯恢复原先函数,我就需要Inline Method (117)。
Extract Method (110)最大的困难就是处理局部变量,而临时变量则是其中一个 主要的困难源头。处理一个函数时,我喜欢运用Replace Temp with Query (120)去掉 所有可去掉的临时变量。如果很多地方使用了某个临时变量,我就会先运用既Split Temporary Variable (128)将它变得比较容易替换。
但有时候临时变量实在太混乱,难以替换。这时候我就需要使用Replace Method with Method Object(135) 它让我可以分解哪怕最混乱的函数,代价则是引入一个新
类。
注释:重构手法后面的括号是指书中的对应处理手法的页数,之所以列出来也是为了读者可以方便去书中直接找对应的处理方法。
下面会一一介绍包括使用逻辑以及注意点:
一、Extract Method (提炼函数)
你有一段代码可以被组织在一起并独立出来。将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。
为什么需要提炼函数呢? 首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来就像一系列注释;再次,如果函数都是细粒度,那么函数的覆写也会更容易些。
做法:
1、创造一个新函数,根据这个函数的意图来对它命名(以它"做什么”来命名, 而不是以它“怎样做”命名)。
2、将提炼出的代码从源函数复制到新建的目标函数中。
3、仔细检査提炼出的代码,看看其中是否引用了"作用域限于源函数”的变量
(包括局部变量和源函数参数)。
4、检査被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时 变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果值赋值给相关变量。
如果很难这样做,或者如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。你可能需要先使用Splite Temporary Variable (128),然后再尝试提炼。也可以使用Temp with Query (120) 将临时变量消灭掉。
5、将被提炼代码段中需要读取的局部变量,当作参数传给目标函数。
6、处理完所有局部变量之后,进行编译。
7、在源函数中,将被提炼代码段替换为对目标函数的调用。
8、如果你将任何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在你可以删除这些声明式了。
9、编译,测试。
如果你发现源函数的参数被赋值,应该马上使用Remove Assignments to Parameters (131)就是将入参的局部变量再赋值给一个同类型的参数。
void method(Integer a, Integer b) {
// Remove Assignments to Parameters
Integer c = a;
c += 1;
}
特殊情况
被赋值的临时变量也分两种情况。较简单的情况是:这个变量只在被提炼代码段中使用。果真如此,你可以将这个临时变量的声明移到被提炼代码段中,然后一 起提炼出去。另一种情况是:被提炼代码段之外的代码也使用了这个变量。这又分 为两种情况:如果这个变量在被提炼代码段之后未再被使用,你只需直接在目标函数中修改它就可以了;如果被提炼代码段之后的代码还使用了这个变量,你就需要让目标函数返回该变量改变后的值。
代码示例:(带有修改局部变量在赋值的情况)
void printowing() (
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
// calculate outstanding while (e.hasMoreElements()) (
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
)
printDetails(outstanding);
)
现在我把“计算”代码提炼出来:
void printowing() (
printBanner();
double outstanding = getOutstanding();
printDetails(outstanding);
}
/** Enumeration变量e只在被提炼代码段中用到,所以可以将它整个搬到新函数
中。double变量outstanding在被提炼代码段内外都被用到,所以必须让提炼出 来的新函数返回它。
*/
double getOutstanding() (
Enumeration e = _orders.elements();
double outstanding = 0.0;
while (e.hasMoreElements()) (
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
return outstanding;
}
本例中的outstanding变量只是很单纯地被初始化为一个明确初值,所以我可 以只在新函数中对它初始化。如果代码还对这个变量做了其他处理,就必须将它的 值作为参数传给目标函数。
如果需要返回的变量不止一个,又该怎么办呢???
有几种选择。最好的选择通常是:挑选另一块代码来提炼。我比较喜欢让每个 函数都只返回一个值,所以会安排多个函数,用以返回多个值。如果你使用的语言 支持“出参数"(output parameter),可以使用它们带回多个回传值。但我还是尽可 能选择单一返回值。
临时变量往往为数众多,甚至会使提炼工作举步维艰。这种情况下,我会尝试 先运用Replace Temp with Query (120)减少临时变量。如果即使这么做了提炼依旧困难重重,我就会动用Replace Method with Method Object (135),这个重构手法不在乎 代码中有多少临时变量,"也不在乎你如何使用它们。
二、Inline Method (内联函数)
一个函数的本体与名称同样清楚易懂。在函数调用点插入函数本体,然后移除该函数。比起提炼函数,内联函数的操作正好相反,
实例:
int getRatingf) {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() (
return _numberOfLateDeliveries > 5;
}
重构后:
int getRating() {
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}
本重构方法好像很简单,但是为了以防万一导致出现其他问题,还是罗列出具体的操作动作。
做法:
1、检査函数,确定它不具多态性。如果子类继承了这个函数,就不要将此函数内联,因为子类无法覆写一个 根本不存在的函数。
2、找出这个函数的所有被调用点。
3、将这个函数的所有被调用点都替换为函数本体。
4、编译,测试。
5、删除该函数的定义。
三、Inline Temp (内联临时变量)
你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。
将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。
示例:
double basePrice = anOrder.basePrice();
return (basePrice > 1000)
重构为:
return (anOrder.basePrice() > 1000)
Inline Temp (119)多半是作为Replace Temp with Query (120)的一部分使用的,所 以真正的动机出现在后者那儿。唯一单独使用Inline Temp情况是:你发现某 个临时变量被赋予某个函数调用的返回值。
实现步骤:
1、检査给临时变量赋值的语句,确保等号右边的表达式没有副作用。
2、如果这个临时变量并未被声明为final,那就将它声明为final,然后编译。=»这可以检查该临时变量是否真的只被赋值一次。
3、找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。
4、每次修改后,编译并测试。
5、修改完所有引用点之后,删除该临时变量的声明和赋值语句。
6、编译,测试。
四、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;
}
这样实现的动机为:
临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用。由于临时 变量只在所属函数内可见,所以它们会驱使你写出更长的函数,因为只有这样你才 能访问到需要的临时变量。如果把临时变量替换为一个査询,那么同一个类中的所 有函数都将可以获得这份信息。这将带给你极大帮助,使你能够为这个类编写更清 晰的代码。
这个重构手法较为简单的情况是:临时变量只被赋值一次,或者赋值给临时变 量的表达式不受其他条件影响。其他情况比较棘手,但也有可能发生。你可能需要 先运用Splite Temporary Variable (128分解临时变量为多个)或 Separate Query from Modifier (279将查询函数和修改函数分离) 使情况变得简单一些,然后再替换临时变量。如果你想替换的临时变量是用来收集结果的(例如循环中的累加值),就需要将某些程序逻辑(例如循环)复制到查询函数去。
做法:
1、找出只被赋值一次的临时变量。
2、今如果某个临时变量被赋值超过一次,考虑使用splite Temporary Variable 将它分割成多个变量。
3、将该临时变量声明为final。-》确保改变量只被赋值一次
4、编译。
5、将"对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中。
5.1 首先将函数声明为private.日后你可能会发现有更多类需要使用它,那时放松对它的保护也很容易.
5.2 确保提炼出来的函数无任何副作用,也就是说该函数并不修改任何对象内 容。如果它有副作用,就对它进行 Separate Query from Modifier (279).
6、编译,测试。
7、在该临时变量身上实现 Inline Temp 。
我们常常使用临时变量保存循环中的累加信息。在这种情况下,整个循环都可 以被提炼为一个独立函数,这也使原本的函数可以少掉几行扰人的循环逻辑。有时 候,你可能会在一个循环中累加好几个值,就像本书第26页的例子那样。这种情况下你应该针对每个累加值重复一遍循环,这样就可以将所有临时变量都替换为查询。
示例如下:
开始的实例:
首先,我从一个简单函数开始:
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;
}
如果有任何问题,编译器就会警告我。之所以先做这件事,因为如 果临时变量不只被赋值一次,我就不该进行这项重构。
重构后:
private int basePrice() (
return _quantity * _itemPrice;
}
private double discountFactor() (
if (basePrice() > 1000) return 0.95;
else return 0.98;
}
最终,getPrice ()变成了这样:
double getPrice() {
return basePrice() * discountFactor();
}
五、 Introduce Explaining Variable (引入解释性变量)
你有一个复杂的表达式。将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
if ( (platform. toUpperCase () . indexOf () > -1) && (browser.toUpperCase().indexOf(nIE") > -1) && waslnitialized() && 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 && waslnitialized().&& wasResized) (
//do something
}
动机:缘由
表达式有可能非常复杂而难以阅读。这种情况下,临时变量可以帮助你将表达 式分解为比较容易管理的形式。你可以用这项 重构将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的 意义。代码可读性强。
做法
做法
1、声明一个final临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它。
2、将表达式中的“运算结果”这一部分,替换为上述临时变量。
3、如果被替换的这一部分在代码中重复出现,你可以毎次一个,逐一替换。
4、编译,测试。
5、重复上述过程,处理表达式的其他部分。
示例
我们从一个简单计算开始:
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);
}
这段代码还算简单,不过我可以让它变得更容易理解。首先我发现,底价(base price)等于数量(quantity)乘以单价(item price)o, 于是,我把这一部分计算的结 果放进一个临时变量中:
稍后也用上了 '‘数量乘以单价"运算结果,所以我同样将它替换为basePrice 临时变量.
批发折扣(quantity discount)的计算提炼出来,将结果赋予临时变量.
最后,我再把运费(shipping)计算提炼出来,将运算结果赋予临时变量 shipping。同时我还可以删掉代码中的注释,因为现在代码已经可以完美表达自 己的意义了:
重构后:
double price() (
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.rnax(Or _quantity - 500)* _itemPrice * 0.05; final double shipping = Math.min(basePrice * 0.1, 100.0);
return basePrice - quantityDiscount + shipping;
}
使用 Extract Method处理上述范例
面对上述代码,我通常不会以临时变量来解释其动作意图,我更喜欢使用 Extract Method(110)。
这一次我把底价计算提炼到一个独立函数中,批发折扣(quantity discount)的计算提炼出来,运费(shipping)计算提炼出来。
最终可以得到如下的代码:
double price() (
return basePrice() - quantityDiscount() + shipping();
)
// 一开始我会把这些新函数声明为private; 如果其他对象也需要它们,我可以轻易释放这些函数的访问限制。
private double quantityDiscount() (
return Math.max(0, .quantity - 500) * _itemPrice * 0.05;
)
private double shipping() (
return Math.min(basePrice() * 0.1, 100.0);
}
private double basePrice() (
return ^quantity * _itemPrice;
}
说明:那么,应该在什么时候使用Introduce Explaining Variable (124)呢?答案是:在 Extract Method (110)需要花费更大工作量时。如果我要处理的是一个拥有大量局部变量的算法,那么使用Extract Method (110)绝非易事。这种情况下就会使用Introduce Explaining Variable (124)来理清代码,然后再考虑下一步该怎么办。搞清楚代码逻 辑之后,我总是可以运用Replace Temp with Query (120)把中间引入的那些解释性临 时变量去掉。况且,如果我最终使用Replace Method with Method Object (135)>那么 中间引入的那些解释性临时变量也有其价值。