重构之重新组织函数
@(常识)
[toc]
那有什么天生如此,只是我们天天坚持。
本篇文章主要讲解 《重构—-改善既有代码的设计》 这本书中的 第六章重新组织函数中 的知识点,
将现有的函数重新分解是 进行大型优雅重构的第一步!
内联函数
问题:一个函数的本体与名称同样清楚易懂。
解决:在函数调用点插入函数本体,然后移除该函数
//重构前
public int getRating(){
return (moreThanSixLateDeliveries()) ? 2 : 1;
}
boolean moreThanSixLateDeliveries(){
return _numberOfLateDeliveries > 6;
}
//重构后
public int getRating(){
return (_numberOfLateDeliveries > 6) ? 2 : 1;
}
动机
重构过程中经常会以简短的函数来表现动作意图,这样就使得代码更清晰易读。
但有时你会遇到某些函数,其内部代码和函数名称同样清晰易读。可能你重构了该函数,使得其内容和其名称变得同样清晰。果真如此,你就应该去掉这个函数,直接使用其中的代码。间接性可能会带来一些帮助,但是没有必要的间接性总是让人感觉不舒服。
还有一种情况是:你手上有一群组织不甚合理的函数。
你可以将它们都内联到一个大型函数中,再从中提炼出组织合理的小型函数。比起既要移动一个函数、又要移动它所调用的其它所有函数,将整个大型函数作为整体来移动会比较的简单。
如果你发现代码中使用了太多的间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,造成对被些委托动作弄的晕头转向,这时通常也会使用内联函数。
做法
(1)检查函数,确定其不具有多态性。(如果子类继承了这个函数,就不要将此函数内联,因为子类无法复写一个根本不存在的函数)。
(2)找出这个函数的所有被调用点。
(3)将这个函数的所有被调用点都替换为函数本体。
(4)编译,测试。
(5)一切正常后,删除该函数的定义。
内联函数看起来似乎很简单。但情况往往那并非如此。对于递归调用、内联至另一个对象中而该对象并无提供访问函数……每一种情况都会很复杂。不介绍复杂情形是因为:如果你遇到了这样复杂的情形,那么就不该运用这种重构手法。
内联临时变量
问题:你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其它的重构手法。
解决:将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。
//重构前
double basePrice = singleOrder.basePrice();
return (basePrice > 1000)
//重构后
return (singleOrder.basePrice() > 1000)
动机
内联临时变量多数情况是作为“以查询取代临时变量”的一部分来进行使用的,而真正的动机是出现在“以查询取代临时变量”中。
唯一单独使用内联临时变量的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说,这样的临时变量是不会造成任何危害的,也可以放心地放在那儿。
但是,如果这个临时变量妨碍了其它的重构手法(例如提炼函数),你就应该将其内联化。
做法
(1)检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
(2)如果这个临时变量并未被声明为final,那就将它声明为final,然后编译。(这可以检查该临时变量是否真的只被赋值一次)
(3)找到该临时变量所有引用点,将它们替换为“为临时变量赋值”的表达式。
(4)每次修改后,编译并测试。
(5)修改完后所有引用点后,删除该临时变量的声明和赋值语句。
(6)编译,测试
引入解释性变量
问题:你有一个复杂的表达式。
解决:将该复杂的表达式(或其中的部分)的结果放进一个临时变量,并以此变量名称来解释表达式用途。
//重构前
if((platform.toUpperCase().indexOf("MAC") > -1) &&
(browser.toUpperCase().indexOf("IE") > -1) &&
wasInitialized() && resize > 0)
{
//do something
}
//重构后
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResize = resize > 0;
if(isMacOs && isIEBrowser && wasInitialized() && wasResize){
//do something
}
动机
在某些情况下,表达式可能非常的复杂以至于难以阅读。这样,临时变量可以帮助你将表达式分解为比较容易管理的形式。
在条件逻辑中,引入解释性变量就显得比较有价值:你可以用这项重构将每个子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。另一种可能的情况是,对于那些比较长的算法,可以运用临时变量来解释每一步运算的意义。
本文的重构手法是比较常见的手法之一,但是对其的使用又不是那么的多。因为一般情况下,我们都可以使用提炼函数来解释一段代码的意义。毕竟临时变量只有在它所处的那个函数中才有意义,局限性较大,函数则可以在对象的整个生命周期中都有用,并且可被其它对象使用。但是,当局部变量使用提炼函数难以进行时,就可以尝试使用引入解释性变量。
做法
(1)声明一个final型的临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它。
(2)将表达式中的“运算结果”这一部分,替换为上述的临时变量。(如果被替换的这一部分在代码中重复出现,可以每次一个,逐一进行替换)
(3)编译,测试。
(4)重复上述过程,处理其它类似部分。
示例
//重构前
double price(){
// 价格 = basePrice - quantity discount + shipping
return _quantity * _itemPrice -
Math.max(0, _quantity - 800) * _itemPrice * 0.15 +
Math.min(_quantity * _itemPrice * 0.25, 100);
}
这段代码还是比较简单,不过现在要让其更加容易理解一些。
首先发现底价(basePrice)等于数量(quantity)乘以单价(item price)。于是可以把这一部分的计算结果放进一个临时变量中,同时将Math.min()函数中参数进行同样替换。
double price(){
// 价格 = basePrice - quantity discount + shipping
final double basePrice = _quantity * _itemPrice;
return basePrice -
Math.max(0, _quantity - 800) * _itemPrice * 0.15 +
Math.min(basePrice * 0.25, 100);
}
然后,将批发折扣(quantity discount)的计算提炼出来,并将运算结果赋予临时变量。
double price(){
// 价格 = basePrice - quantity discount + shipping
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.max(0, _quantity - 800) * _itemPrice * 0.15;
return basePrice -quantityDiscount+
Math.min(basePrice * 0.25, 100);
}
最后,再把搬运费(shipping)计算提炼出来,并将运算结果赋予临时变量。
//重构后
double price(){
// 价格 = basePrice - quantity discount + shipping
final double basePrice = _quantity * _itemPrice;
final double quantityDiscount = Math.max(0, _quantity - 800) * _itemPrice * 0.15;
final double shipping = Math.min(basePrice * 0.25, 100);
return basePrice - quantityDiscount + shipping;
}
运用提炼函数处理
对于上述代码,通常不以临时变量来解释其动作意图,而是更喜欢使用提炼函数。
//重构前
double price(){
// 价格 = basePrice - quantity discount + shipping
return _quantity * _itemPrice -
Math.max(0, _quantity - 800) * _itemPrice * 0.15 +
Math.min(_quantity * _itemPrice * 0.25, 100);
}
现在把底价计算提炼到一个独立的函数中。
double price(){
// 价格 = basePrice - quantity discount + shipping
return basePrice() -
Math.max(0, _quantity - 800) * _itemPrice * 0.15 +
Math.min(basePrice() * 0.25, 100);
}
private double basePrice(){
return _quantity * _itemPrice;
}
继续进行提炼,每次提炼一个新的函数。最后得到代码如下。
//重构后
double price(){
// 价格 = basePrice - quantity discount + shipping
return basePrice() - quantityDiscount() + shipping();
}
private double basePrice(){
return _quantity * _itemPrice;
}
private double shipping(){
return Math.min(basePrice() * 0.25, 100);
}
private double quantityDiscount(){
return Math.max(0, _quantity - 800) * _itemPrice * 0.15;
}
分解临时变量
问题:你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。
解决:针对每次赋值,创造一个独立、对应的临时变量。
//重构前
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);
动机
在某些情况下,临时变量用于保存一段冗长代码的运算结果,以便稍后使用。
这种临时变量应该只被赋值一次。
如果它被赋值超过一次,就意味着它们在函数中承担了一个以上的责任。
如果临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,使得每一个变量只承担一个责任。
同一个临时变量承担两件不同的事情,会让代码阅读者糊涂。
做法
(1)在待分解临时变量的声明及第一次被赋值处,修改其名称。
(2)将新的临时变量声明为final。
(3)以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。
(4)在第二次赋值处,重新声明原先那个临时变量。
(5)编译,测试。
(6)逐次重复上述过程。每次都在声明处对临时变量改名,并修改下次赋值之前的引用点。
示例
我们从一个简单计算开始:我们需要计算一个苏格兰布丁运动的距离。在起点处,静止的布丁会受到一个初始力的作用而开始运动。一段时间后,第二个力作用于布丁,让它再次加速。根据牛顿第二定律,计算布丁运动距离:
牛顿第二定律
内容:物体的加速度与所受合外力成正比,跟物体的质量成反比。
表达式:F=ma。
物理意义:反映物体运动的加速度大小、方向与所受合外力的关系,且这种关系是瞬时的。
double getDistance(int time){
double result;
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。然后,把第二次赋值之前对acc变量的所有引用点,全部改用心的临时变量。最后,在第二次赋值处重新声明acc变量。
double getDistance(int time){
double result;
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;
double acc = (_primaryForce + _secondaryForce) / _mass;
result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
}
return result;
}
新的临时变量指出,它只承担原先acc变量的第一个责任。
将它声明为final,确保它只被赋值一次。
然后,在原先acc变量第二次被赋值处重新声明acc。
现在,重新编译并测试,一切都没有问题。
然后继续处理acc临时变量的第二次赋值
double getDistance(int time){
double result;
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;
final double secondaryAcc = (_primaryForce + _secondaryForce) / _mass;
result += primaryVel * secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime;
}
return result;
}
以查询取代临时变量 手法进行重构
//“以查询取代临时变量”手法进行重构
double getDistance(int time){
double result= 0.5 * getPrimaryAcc() * getPrimaryTime(time) * getPrimaryTime(time);
if(getSecondaryTime(time) > 0){
result += getSeconddistance();
}
return result;
}
private double getPrimaryAcc(){
return _primaryForce / _mass;
}
private double getSecondaryAcc(){
return (_primaryForce + _secondaryForce) / _mass;
}
private int getPrimaryTime(int time){
return Math.min(time, _delay);
}
private int getSecondaryTime(int time){
return time - _delay;
}
private double getSeconddistance(){
return getPrimaryAcc() *_delay * getSecondaryTime(time)
+ 0.5 * getSecondaryAcc() * getSecondaryTime(time) * getSecondaryTime(time);
}
移除对参数的赋值
问题:代码对一个参数进行赋值。
解决:以一个临时变量取代该参数的位置。
//重构前
int dicount(int inputVal, int quantity, int yearToDate){
if(inputVal > 50) inputVal-=10;
}
//重构后
int dicount(final int inputVal, int quantity, int yearToDate){
int result = inputVal;
if(result > 50) result-=10;
}
动机
我想你很清楚“对参数赋值”这个说话的意思。
如果把一个名称为fool的对象作为参数传递给某个函数,那么“对参数赋值”意味改变fool,使它引用另一个对象。
但是,如果在“被传入对象”身上进行什么操作,那没问题,我们经常会这么做。
这里只针对“fool被改变而指向另一个对象”这种情况来讨论:
void test(Object fool){
fool.changedBySomeWay(); //that's ok
fool=anotherObject; //trouble will appear
}
我们之所不这样做,是因为它降低了代码的清晰度,而且混用了按值传递和按引用传递这两种参数传递方式。
JAVA只采用按值进行传递。
在按值传递的情况下,对参数的任何修改,都不会对调用端造成任何影响。
如果你只以参数表示“被传递进来的东西”,那么代码会清晰的多,因为这种用法在所有语言中都表现出相同的语义。
在JAVA中,一般不要对参数赋值:如果你看到手上的代码已经这么做了,就应该使用本文的方法。
做法
(1)建立一个临时变量,把待处理的参数值赋赋予它。
(2)以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为“对此临时变量的引用”。
(3)修改赋值语句,使其改为对新建之临时变量赋值。
(4)编译,测试。
(如果代码的语义是按照引用传递的,需在调用端检查调用后是否还使用了这个参数。也要检查有多少个按引用传递的参数被赋值后又被使用。应该尽量以return方式返回一个值。如果返回值有多个,可考虑将需返回的一大堆数据变为对象,或者为每个返回值设定一个独立的函数)
示例
int dicount(int inputVal, int quantity, int yearToDate){
if(inputVal > 50) inputVal-=5;
if(quantity > 100) quantity-=10;
if(yearToDate > 1000) yearToDate-=100;
return inputVal;
}
以临时变量取代对参数的赋值动作,得到下列代码:
int dicount(int inputVal, int quantity, int yearToDate){
int result = inputVal;
if(result > 50) result-=5;
if(quantity > 100) quantity-=10;
if(yearToDate > 1000) yearToDate-=100;
return result;
}
可以为参数加上final关键词,强制其遵循“不对参数赋值”这一惯例:
int dicount(final int inputVal, final int quantity, final int yearToDate){
int result = inputVal;
if(result > 50) result-=5;
if(quantity > 100) quantity-=10;
if(yearToDate > 1000) yearToDate-=100;
return result;
}
JAVA的按值传递
我们应该都知道,JAVA使用按值传递的函数调用方式,这常常也会使大家迷惑。在所有地点,JAVA都会遵循严格按值传递:
//JAVA按值的传递
class Params{
public static void main(String[] args) {
int x = 10;
triple(x);
System.err.println("x after triple:" + x);
}
private static void triple(int arg){
arg = arg * 3;
System.err.println("arg in triple:" +arg );
}
}
//输出
//arg in triple:30
//x after triple:10
上面代码是使用基本数据类型进参数传递,还不至于让人糊涂。但如果参数中传递的是对象,就可能把人弄糊涂。如果在程序中以Date对象表示日期,下列程序所示:
//以对象为参数
class Params{
public static void main(String[] args) {
Date d1 = new Date(2015,1,1);
nextDateUpdate(d1);
System.err.println("d1 after nextday:" + d1);
Date d2 = new Date(2015,1,1);
nextDateReplace(d2);
System.err.println("d2 after nextday:" + d2);//61380864000000
}
private static void nextDateUpdate(Date d) {
d.setDate(d.getDate()+1);
System.err.println("arg in nextday d1 : "+d);
}
private static void nextDateReplace(Date d) {
d = new Date(d.getYear(),d.getMonth(),d.getDate()+1);
d=null;
System.err.println("arg in nextday d2: "+d);
}
}
//输出
/*
arg in nextday d1 : Tue Feb 02 00:00:00 CST 3915
d1 after nextday: Tue Feb 02 00:00:00 CST 3915
arg in nextday d2: Tue Feb 02 00:00:00 CST 3915
d2 after nextday: Mon Feb 01 00:00:00 CST 3915
*/
从本质上说,对象的引用是按值传递的。因为可以修改参数对象的内部状态,但对参数对象重新赋值是没有意义的。
以函数对象取代函数
问题:你有一个大型函数,其中对局部变量的使用使你无法采用“提炼函数”这种重构手法。
解决:将这个函数放进一个单独对象中,这样,局部变量就成了对象的字段,然后就可以在同一个对象中将这个大型函数分解为多个小型函数。
//重构前
class Order....
double price(){
double basePrice;
double secondaryPrice;
double thirdaryPrice;
//compute()
......
}
//重构后
class Order...
double price(){
return new PriceCalculator(this).compute();
}
class PriceCalculator{
double basePrice;
double secondaryPrice;
double thirdaryPrice;
double compute(){
//...
}
}
动机
在前面的文章中一直在强调小型函数的优美动人。
只要将相对独立的代码从大函数提炼出来,就可以大大提高代码的可读性。
但是,局部变量的存在会增加函数分解的难度。
如果一个函数中的局部变量泛滥成灾,那么想分解这个函数是非常困难的。
“以查询替换临时变量”手法可以帮助减轻负担,但有时候还是会发现根本无法拆解一个需要拆解的函数。
这种情况就应该考虑使用函数对象来解决。本文的重构方法会将所有的局部变量都变成函数对象的字段。然后就可以使用“提炼函数”创造新的函数,从而将原来的大型函数拆解变小。
做法
(1)建立一个新的类,根据待处理函数的用途为其命名。
(2)在新类中建立一个final字段,用以保存原先大型函数所在对象。针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保留之。
(3)在新类中建立一个构造函数,接收原对象以原函数的所有参数作为其参数。
(4)在新类中建立一个compute()函数。
(5)将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,通过原对象字段调用。
(6)编译,测试。
由于所有局部变量现在都成了字段,所以你可以任意分解这个大型函数,不必传递任何参数。
示例
class Account{
int gamm(int value, int quantity, int year2Date){
int importValue1 = (value * quantity) + delta();
int importValue2 = (value * year2Date) + 200;
if(year2Date - importValue1 >200)
importValue2-=50;
int importValue3 = importValue2 * 8;
//......
return importValue3 - 2 * importValue1;
}
//.....
}
为了把这个函数变为函数对象,首先需要声明一个新类。在新类中提供final对象保存原对象,对于函数的每个参数和每个临时变量,也以一个字段逐一保留。
class Gamm{
private final Account _account;
private int value;
private int quantity;
private int year2Date;
private int importValue1;
private int importValue2;
private int importValue3;
接下来,加入一个构造函数。
Gamm(Account source, int inputVal, int quantity, int year2Date){
this._account = source;
this.value = inputVal;
this.quantity = quantity;
this.year2Date = year2Date;
}
现在可以把原本函数搬到compute()中了。函数中任何调用Accout类的地方,都必改用_account字段。
int compute(){
importValue1 = (value * quantity) + _account.delta();
importValue2 = (value * year2Date) + 200;
if(year2Date - importValue1 >200)
importValue2-=50;
importValue3 = importValue2 * 8;
//......
return importValue3 - 2 * importValue1;
}
然后,修改旧函数,让它将工作委托给刚完成的这个函数对象。
int gamm(int value, int quantity, int year2Date){
return new Gamm(this,value,quantity,year2Date).compute();
}
以上就是本文重构方法的基本原则。其所带来的好处是:现在可以轻松地对compute()函数采取“提炼函数”,而不必担心参数传递的问题。
//运用提炼函数 不必担心参数问题
int compute(){
importValue1 = (value * quantity) + _account.delta();
importValue2 = (value * year2Date) + 200;
importantThing();
importValue3 = importValue2 * 8;
//......
return importValue3 - 2 * importValue1;
}
private void importantThing() {
if(year2Date - importValue1 >200)
importValue2-=50;
}
主要介绍了重构手法——以函数对象取代函数。
我们都不喜欢临时变量巨多的方法,那只会让我们迷惑。
对于局部变量很多的函数,有必要运用本文的重构方法进行处理,将其转化为函数对象,那样就把临时变量转为函数对象的字段,继而可以进行其它重构方法。