重构—改善既有代码的设计006:重新组织你的函数(Composing Methods)
一:EXTRACT METHOD
二:INLINE METHOD
三:INLINE TEMP
四:REPLACE TEMP WITH QUERY
五:INTRODUCTION EXPLAINING VARIABLE
六:SPLIT TEMPORARY VARIABLE
七:REMOVE ASSIGNMENTS TO PARAMETERS
八:REPLACE METHOD WITH METHOD OBJECT
九:SUBSTITUTE ALGOTITHM
一:EXTRACT METHOD
1:如果你有一段代码可以被组织在一起并独立出来
则将这段代码放进一个独立函数中,并让函数名称解释该函数的用途
void printOwing(double amount)
{
printBanner();
Console.WriteLine("name:" + _name);
Console.WriteLine("amount:" + amount);
}
void printOwing(double amount)
{
printBanner();
printDetails(amount);
}
void printDetails(double amount)
{
Console.WriteLine("name:" + _name);
Console.WriteLine("amount:" + amount);
}
2:动机:函数名和函数本体之间的意义距离
首先:如果每个函数的粒度都很小,那么函数之间彼此服用的机会就会更大
其次:这回使高层函数读起来就像一序列注释
再者:如果函数都是细粒度,那么函数的覆盖也会更容易些
3:做法:
1):创造一个新函数,根据这个函数的意图来给它命名,以它做什么来命名而非以它怎样做命名
2):将提炼出的代码从源函数拷贝到新建的目标函数中
3):仔细检查提炼出的代码,看看是否其中引用了作用域的变量,包括局部变量和源函数参数
4):将被提炼代码中需要读取的局部变量当做参数传给目标函数
5):如果有临时变量,看看能够提炼成一个查询
6):处理完所有的局部变量后,进行编译
7):在源函数中,将被提炼码替换为对目标函数的调用
临时变量往往位数众多,甚至会使提炼工作举步维艰,这是可以先尝试REPLACE TEMP WITH QUERY减少临时变量
4:范例
这个重构手法的困难点在于局部变量,包括传进源函数的参数和源函数所声明的局部变量。
局部变量的作用域仅限于源函数,所以当使用此方法时,必须花费额外功夫去处理这些变量。
1):局部变量最简单的情况是:被提炼的代码只是读取这些变量的值,并不修改它们。
这种情况可以简单的将它们当做参数传给目标函数。
2):如果被提炼的局部变量赋值,问题就变得复杂了。
此处只讨论临时变量被赋值,如果发现源函数参数被赋值,则用REMOVE ASSIGNMENTS TO PARAMETERS
被赋值的链式变量分为两种情况
.这个变量只在被提炼的区域中使用,这是可以将这个临时变量的声明移到被提炼中,然后一起提炼出去
.被提炼之外的代码也是用这个变量,又分两种情况
..如果这个变量在被提炼之后未被再使用,只需直接在目标函数中修改它即可
..如果被提炼代码之后的代码还是用了这个编码,就需要让目标函数返回该变量改变后的值。
void printOwing()
{
ArrayList array = new ArrayList();
double outstanding = 0;
printBanner();
for (int index = 0; index < array.Count;index++ )
{
object tmp = array[index];
outstanding += tmp.data;
}
printDetails(outstanding);
}
修改为:
void printOwing()
{
printBanner();
double outstanding = getOutstanding();
printDetails(outstanding);
}
double getOutstanding()
{
ArrayList array = new ArrayList();
double result = 0;
for (int index = 0; index < array.Count; index++)
{
object tmp = array[index];
result += tmp.data;
}
return result;
}
当代码还对此变量做了其他处理,就必须把它的值作为参数传给目标函数
void printOwing(double previousAmount)
{
printBanner();
double outstanding += getOutstanding(previousAmount * 2);
printDetails(outstanding);
}
double getOutstanding(double initValue)
{
ArrayList array = new ArrayList();
double result = initValue;
for (int index = 0; index < array.Count; index++)
{
object tmp = array[index];
result += tmp.data;
}
return result;
}
二:Inline Method
1:在函数调用点插入函数本体,然后移除该函数
int getRating()
{
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
Boolean moreThanFiveLateDeliveries()
{
return _numberOfLateDeliveries > 5;
}
int getRating()
{
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}
2:动机
某些函数其内部代码和函数名同样清晰易懂,这是就应该去掉这个函数。间接性可能带来帮助,但非必要的间接性总是让人不舒服
有些碰到一些不甚合理的函数,可以考虑将它们都INLINE到一个大型函数中,再从中提炼出组织合理的小型函数。
3:做法
.检查函数,确定它不具多态性
.找出这个函数的所有被调用点
.将这个函数的所有被调用点都替换为函数的本体
.编译,测试
.删除该函数的定义
对于递归调用,多返回点,INLINING至另一个对象中而该元素并无提供访问函数等各种情况,这写就不应该用这个重构手法
三:Inline Temp
1:有一个临时变量,只被一个简单赋值一次,而它妨碍了其他重构手法
将所有对该变量的引用动作,替换为对它赋值的那个表达式本身
double basePrice = anOrder.basePrice();
return (basePrice > 1000);
return (anOrder.basePrice() > 1000)
2:动机
该方法多半作为REPLACE TEMP WITH QUERY的一部分使用。
唯一单独使用的情况就是发现某个临时变量被赋予某个函数调用的返回值,这样的临时变量不会有任何危害,可以放心的把它留在那儿。
3:做法
.如果这个临时变量未被声明为FINAL,那就将其声明为FINAL。这样可以检测该临时变量是否只被编译赋值一次。
.找到该临时变量的所有引用点,将它们替换为“临时变量赋值”语句中的等号右侧表达式
.每次修改后,编译并测试
.修改完所有引用点后,删除该临时变量的声明式和赋值语句
.编译,测试
四:REPLACE TEMP WITH QUERY
1:当程序以一个临时变量保存某一表达式的运算结果
将这个表达式提炼到一个独立函数中,将这个临时变量的所有被引用点替换为对新函数的调用,新函数可悲其他函数调用。
double basePrice = _quality * _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 _quality * _itemPrice;
}
2:动机:临时变量时暂时的,而且只能在所属函数内使用。
REPLACE TEMP WITH QUERY 是用 EXTRACT METHOD之前必不可少的一个步骤。局部电路会使代码难以被提炼,应该尽可能把它们替换为查询式。
直接情况:临时变量只被赋值一次,或者赋值给临时变量的表达式不受其他条件的影响
其他情况可能需要借助SPLIT TEMPORARY VARIABLE 或 SEPERATE QUERY FROM MODIFIER使情况变得简单一些,然后再替换临时变量
3:做法
.找出只被赋值一次的临时变量,如果赋值多次,使用SPLIT TEMPORARY VARIABLE分割成多个变量
.将该临时变量声明为FINAL,编译,确保该临时变量只被赋值一次
.将该临时变量等号右侧部分提炼到一个独立函数中
.编译,测试
.在该临时变量上实施INLINE TEMP
有时用临时变量保存循环中的累加信息,这是整个循环可以被提炼为一个独立的函数。
如果循环中累加好几个值,这是就针对每个累加值重复一遍循环,这样就可以将所有临时变量都替换为查询式。
double getPrice()
{
int basePrice = _quantity * _itemPrice;
double discounterFactor;
if (basePrice > 1000)
discounterFactor = 0.95;
else
discounterFactor = 0.98;
return basePrice * discounterFactor;
}
替换成:
double getPrice()
{
return basePrice() * discounterFactor();
}
int basePrice()
{
return _quantity * _itemPrice;
}
double discounterFactor()
{
if (basePrice() > 1000) reuturn 0.95;
else reuturn 0.98;
}
五:INTRODUCE EXPLAINING VARIABLE
1:你有一个复杂的表达式
将该复杂表达式或其中一部分的结果放进一个临时变量,以此变量名称来解释表达式的用途
if ((platform.toUpperCase().indexOf("MAC") > -1) &&
(browser.toUpperCase().indexOf("IE") > -1) &&
wasInitialized() &&
resize > 0)
{
}
修改为:
finally Boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
finally Boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
finally Boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized)
{
}
2:动机
表达式非常复杂的情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式
在条件逻辑中,INTRODUCE EXPLAINING VARIABLE非常有价值。
3:做法
.声明一个FINAL临时变量,将带分解之复杂表达式中的一部分动作的运算结果赋值给它
.将表达式中的这一部分替换为上述临时变量
.编译,测试
.重复上述过程,处理表达式其它部分
4:范例
double price()
{
return _quality * _itemPrice +
Math.Max(0, _quality - 500) * itemPrice * 0.05 +
Math.Min(_quality * itemPrice * 0.1, 100);
}
double price()
{
finally double basePrice = _quality * _itemPrice;
finally double qualityDiscount = Math.Max(0, _quality - 500) * itemPrice * 0.05;
finally double shipping = Math.Min(_quality * itemPrice * 0.1, 100);
return basePrice + qualityDiscount + shipping;
}
也可以用EXTRACE METHOD重构
double price()
{
return basePrice() + qualityDiscount() + shipping();
}
private double basePrice()
{
return _quality * _itemPrice; ;
}
private double qualityDiscount()
{
return Math.Max(0, _quality - 500) * itemPrice * 0.05;
}
private double shipping()
{
return Math.Min(_quality * itemPrice * 0.1, 100);
}
六:SPLIT TEMPORARY VARIABLE
1:当你的程序有个临时变量被赋值超过以此,它既不是循环变量,也不是一个集用临时变量(COLLECTING TEMPORARY VARIABLE).
针对每次赋值,创造一个独立的,对性的临时变量。
double temp = 2 * (_height + _width);
Console.WriteLine(temp.ToString());
temp = _height * _width;
Console.WriteLine(temp.ToString());
修改后
finally double perimeter = 2 * (_height + _width);
Console.WriteLine(perimeter.ToString());
finally double area = _height * _width;
Console.WriteLine(area.ToString());
2:动机
临时变量有各种小用途,其中某些用户会很自然导致临时变量被多次赋值。如循环变量和集用临时变量。
除了这两种情况外,还有很多临时变量用来保存一段冗余长代码的运算结果,以便稍后使用。
这种临时变量应该只被赋值一次,如果它们被赋值超过以此,就意味着它们在函数中承担一个以上的责任。
如果临时变量承担多个责任,它就应该被替换为多个临时变量。
每个变量通常只承担一个责任,同一个临时变量承担两件不同的事情会令代码阅读者糊涂。
3:做法
.在剖解临时变量机第一次被赋值时,修改其名称
.将新的临时变量声明为FINAL
.以该临时变量第二次赋值运动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量
.在第二次赋值处,重新声明原来那个临时变量
.编译,测试
.逐渐重复上述过程,每次都在声明处对临时变量易名。并修改下次赋值之前的引用点。
七:REMOVE ASSIGNMENTS TO PARAMETERS
1:你的代码对一个参数进行赋值动作,以一个临时变量取代代数的位置
int discount(int inputValue, int quantity, int yearToDate)
{
if (inputValue > 50) inputValue -= 2;
}
修改为
int discount(int inputValue, int quantity, int yearToDate)
{
int result = inputValue;
if (inputValue > 50)
result -= 2;
}
2:动机:
可以降低代码的清晰度,而且混淆了PASS BY VALUE 和 PASS BY REFERENCE这两种参数传递方式。
3:做法
.建立一个临时变量,把待处理的参数赋予它
.以对参数的赋值动作为界,将其后所有对此参数的引用点全部替换为对此临时变量的引用动作
.修改赋值语句,使其改为对新建之临时变量赋值
.编译,测试
4:范例
int discount(int inputValue, int quantity, int yearToDate)
{
if (inputValue > 50) inputValue -= 2;
if (quantity > 100) inputValue -= 1;
if (yearToDate > 10000) inputValue -= 4;
return inputValue;
}
int discount(int inputValue, int quantity, int yearToDate)
{
int result = inputValue;
if (inputValue > 50) result -= 2;
if (quantity > 100) result -= 1;
if (yearToDate > 10000) result -= 4;
return result;
}
八:REPLACE METHOD WITH METHOD OBJECT
1:有一个大型函数,其中对局部变量的使用,是你无法采用EXTRACT METHOD
将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的值域,然后你可以在同一个对象中将这个大型函数分解为数个小型函数。
2:动机
如果一个函数之中局部变量泛滥成灾,则像分解这个函数是非常困难的。
REPLACE TEMP WITH QUERY可以助你减去这一负担,但有时候你会发现根本无法拆解一个需要拆解的函数,这是,可以考虑此方法。
将所有局部变量编程函数对象的值域,然后你就可以对这个新对象使用EXTRACT METHOD创造出新函数,从而将原本的大型函数拆解变短。
3:做法
.建立一个新类,
.在新类中建立一个FINAL值域,保存原大型函数所驻对象。
.在新类中建议一个构造函数,接收源对象及原函数的所有参数作为参数
.在新类中建议一个COMPUTE()函数
.将原函数的代码拷贝到COMPUTE()函数中,如果需要调用源对象任何函数,用源对象值域调用
.编译
.将旧函数的函数本体替换为这样一条语句,常见上述新类对象,而后调用其中的COMPUTE函数
4:范例
class Acount
{
int gamma(int inputValue, int quantity, int yearToDate)
{
int importantValue1 = (inputValue * quantity) + delta();
int importantValue2 = (inputValue * quantity) + 100;
if ((yearToDate - importantValue1) > 100) importantValue2 -= 20;
int importantValue3 = importantValue2 * 7;
return importantValue3 - 2 * importantValue1;
}
}
class Acount
{
int gamma(int inputValue, int quantity, int yearToDate)
{
return new Gamma(this, inputValue, quantity, yearToDate);
}
}
class Gamma
{
private finally Acount _acount;
private int inputValue;
private int quantity;
private int yearToDate;
private int importantValue1;
private int importantValue2;
private int importantValue3;
Gamma(Acount source, int inputValArg, int quantityArg, int yearToDataArg)
{
_acount = source;
inputValue = inputValArg;
quantity = quantityArg;
yearToDate = yearToDataArg;
}
int compute()
{
int importantValue1 = (inputValue * quantity) + _acount.delta();
int importantValue2 = (inputValue * quantity) + 100;
if ((yearToDate - importantValue1) > 100) importantValue2 -= 20;
int importantValue3 = importantValue2 * 7;
return importantValue3 - 2 * importantValue1;
}
}
九:SUBSTITUTE ALGORITHM
1:想要把某个算法替换为另一个更清晰地算法
将函数本体替换为另一个算法
2:做法
.准备好你的另一个替换用的算法,让它通过编译
.针对现有测试,执行上述算法,如果结果和原本结果相同,则重构结束
.如果不同,则与算法为比较参照标准。