重新组织函数
extract method(提炼方法)
当一个函数过长,或者需要注释才能够让人理解用途的代码,需要使用extract method;
extract method 需要做到细化函数粒度、提炼函数名;
TODO:
- 细化原先函数粒度。创造一个新函数、提炼函数名;
- 将提炼出的代码从源函数复制到新函数;
- 处理局部变量、临时变量(作用域限于源函数)(Replace temp with query / Replace method with method object);
- 检查是否有“仅用于被提炼函数代码段”的临时变量,如果有,在目标函数中声明为临时变量。
- 检查被提炼代码段是否可以被处理为一个查询,并将结果复制给源函数相关的临时变量;或者存在多个被改变的临时变量,首先使用split temporary variable处理临时变量。
- 将被提炼代码段中需要使用的变量, 当作参数传递给新函数;
- 在目标函数中将对被提炼代码段的调用替换成对目标函数的调用;
优点:降低函数的粒度,提高函数复用机会;使高层函数读起来像一系列注释;
inline method(内联函数)
当一个函数的本体和它的名称一样清晰易懂时,将这个函数本体添加到调用它的目标函数中, 同时删除这个函数。
另一种使用inline method的情况是:当你受伤有一些函数的组织不合理,此时把通过inline method把这些函数内联,然后再通过extract method提炼出合理的新函数。
一般通过提炼函数的手法重构之后发现如果有这样的情况,应该撤销提炼:“提炼的函数名称与其本体的逻辑皆清晰易懂”
TODO:
- 检查函数,确定它不具有多态性;
- 找出这个函数的所有被调用点;
- 将这个函数的所有被调用点都替换成函数本体;
inline temp(内联临时变量)
内联临时变量,举个例子吧:
// old
int c = 2, d = 3;
int a = c *d;
return a;
// new
int c = 2, d = 3;
return c * d;
TODO:
- 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用;
- 如果这个临时变量没有被声明为final,声明为final(此步骤是为了验证这个变量是否只被赋值一次)
- 找出这个临时变量,(return expr, delete the temp variable)
inline temp的概念很简单,我已经有了这个编程理念
replace temp with query(以查询替代临时变量)
源函数的某一个临时变量存储某一代码段的逻辑执行结果,则此时把该代码段使用extract method 提炼到一个新的函数,然后使用相应的临时变量接受该新函数的返回值;
TODO:
- 找到一个只被赋值一次的临时变量;
- 将该临时变量的计算逻辑提炼到一个新的函数中;
- 使用inline temp理念收尾此次重构;
《重构》6.4节,电子书147页,纸质书123页,有一个简单却很牛逼的示例;
introduce explaining variable(引入解释性变量)
将复杂的表达式的结果使用命名有明确意义的临时变量存储;比如在较长的算法中使用临这样的临时变量,有助于很好地的诠释算法的逻辑意义。
这种重构的技巧与extract method类似。当有很多的局部变量的时候,考虑使用introduce explaining variable;当临时变量较少,extract method看起来更不错;
TODO:
- 声明一个final变量,将复杂表达式的结果赋值给这个变量;
- 替换所有用到这个临时变量代表的复杂表达式的地方;
split temporary variable(分解临时变量)
“循环收集结果变量”,“结果收集变量”,“某些把一些复杂的中间运算结果存储到一个临时变量中”,这些都是典型的导致一个变量被多次赋值的情况。应该遵循每个变量承担一个单独的责任的原则,如果一个临时变量在一个函数中承担了多个责任,那么这个责任就要重新分担给多个临时变量;
TODO:
- 在待分解临时变量第一次赋值处,修改其名称;
- 替换所有使用此临时变量的地方,除了其被再次赋值的地方;
- 在待分解临时变量第二次赋值处,声明新的final变量;
- 在待分解的临时变量的第二次赋值处,将其赋值给新的临时变量;
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;
}
TODO:
- 建立临时变量;
- 把传入的参数赋值给临时变量(对于一一些弱类型语言来说,比如高python的list,可能需要使用列表生成的写法来为一个临时变量赋值)
replace method with method object(以函数对象取代函数)
对于一个巨大的函数,无法使用extract method的手法进行重构,而且之中还穿插着很多的临时变量,此时可以考虑使用当前讲述的重构手法进行重构。
把函数替换成函数对象,也就是以类的方式去重构这个函数,这个函数中使用到的临时变量就成为了此类的属性,此时再使用extract method的重构手法进行重构,将一个大函数分解成多个小功能的函数。
TODO:
- 建立一个新类,为这个类起一个可读性友好的类名称。
- 在新类中创建一个final字段,用以保存原先大型函数所在的对象,我们将这个对象称之为源对象。同时,针对于原来函数的每个临时变量和参数,在新类中建立一个与之对应的字段保存。
- 在新类中创建一个构造函数,接受源对象以及原函数的所有参数作为参数。
- 在新类中建立一个compute()函数
- 将原函数的代码赋值到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。
- 将旧函数的本体替换成这样一条语句:“创建上诉新类的一个新对象,而后调用其中的compute()函数”
实例:电子书160页,实体书136页,有一个很牛逼的实例,一看就懂;范例在文章后面有补充。
substitute algorithm(替换算法)
将某一个算法替换成一个更清晰的算法。
TODO:
- 准备好一个替换算法
- 针对现有测试,执行上诉算法,如果与原先的算法结果一直,则重构结束。
replace method with method object范例
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;
//and so on...
return importantValue3 - 2 * importantValue1;
}
public int Delta()
{
return 100;
}
}
为了把这个函数变成函数对象,首先声明一个新类。在新类中,提供一个字段用于保存原对象,同时也对函数的每个参数和每个临时变量,提供字段用于保存。
class Gamma
{
private readonly Account _account;
private readonly int _inputVal;
private readonly int _quantity;
private readonly int _yearToDate;
private int _importantValue1;
private int _importantValue2;
private int _importantValue3;
}
接下来,加入一个构造函数:
public Gamma(Account account, int inputVal, int quantity, int yearToDate)
{
_account = account;
_inputVal = inputVal;
_quantity = quantity;
_yearToDate = yearToDate;
}
接下来,将原本的函数搬到Compute()中。
public int Compute()
{
_importantValue1 = _inputVal * _quantity + _account.Delta();
_importantValue2 = _inputVal * _yearToDate + 100;
if (_yearToDate - _importantValue1 > 100)
{
_importantValue2 -= 20;
}
_importantValue3 = _importantValue2 * 7;
//and so on...
return _importantValue3 - 2 * _importantValue1;
}
完整的Gamma函数如下:
class Gamma
{
private readonly Account _account;
private readonly int _inputVal;
private readonly int _quantity;
private readonly int _yearToDate;
private int _importantValue1;
private int _importantValue2;
private int _importantValue3;
public Gamma(Account account, int inputVal, int quantity, int yearToDate)
{
_account = account;
_inputVal = inputVal;
_quantity = quantity;
_yearToDate = yearToDate;
}
public int Compute()
{
_importantValue1 = _inputVal * _quantity + _account.Delta();
_importantValue2 = _inputVal * _yearToDate + 100;
if (_yearToDate - _importantValue1 > 100)
{
_importantValue2 -= 20;
}
_importantValue3 = _importantValue2 * 7;
//and so on...
return _importantValue3 - 2 * _importantValue1;
}
}
最后,修改旧函数,让它的工作委托给刚完成的这个函数对象。
int Gamma(int inputVal, int quantity, int yearToDate)
{
return new Gamma(this, inputVal, quantity, yearToDate).Compute();
}
这就是本项重构的基本原则。它的好处是:现在我们可以轻松地对Compute()函数采取 Extract Method
,不必担心参数传递的问题。
比如说我们对Compute进行如下重构:
public int Compute()
{
_importantValue1 = _inputVal * _quantity + _account.Delta();
_importantValue2 = _inputVal * _yearToDate + 100;
GetImportantThing();
_importantValue3 = _importantValue2 * 7;
//and so on...
return _importantValue3 - 2 * _importantValue1;
}
void GetImportantThing()
{
if (_yearToDate - _importantValue1 > 100)
{
_importantValue2 -= 20;
}
}