美文网首页《重构》读书笔记
《重构》学习笔记(04)-- 重新组织函数

《重构》学习笔记(04)-- 重新组织函数

作者: 若隐爱读书 | 来源:发表于2019-06-08 17:39 被阅读0次

重构的手法中,很大的一部分就是对函数进行处理,使之更恰当的包装代码。一般公司的编程规范中,都会对函数长度进行限制(例如不能超过50行)。针对过长函数需要进行逻辑抽取,抽取过程中会使用以下的方法。

Extract Method(提取函数)

提取函数是最常用的手法之一,在大厂的编程规范中,都会对函数长度做限制,例如50行。函数提炼有三个好处:

  1. 如果函数颗粒度很小,那么函数被复用的机会就更大。
  2. 函数名称可以起到注释的作用,使程序读起来更加流畅。
  3. 如果函数都是细颗粒度,那么函数的覆写也会更容易一些。

提取函数难点和重点在于临时变量的处理。一般的做法如下:

  • 创造一个新函数,根据这个函数的意图对它命名(以它“做什么”来命名,而不是“怎么做”命名)。
  • 将提炼出来的代码从源文件复制到新建的目标函数中。
  • 仔细检查提炼出的代码,是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。
  • 检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将它们声明为临时变量。
  • 检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果有一个临时变量被修改了,看看是否可以提炼为一个查询。如果很难这样做,那么就不能原封不动的提炼代码段,需要用到Split Temporary Variable或者Replace Temp with Query将临时变量消灭掉。
  • 将被提炼代码段中需要读取的局部变量,当作参数传递给目标函数。
  • 处理完所有局部变量以后,进行编译。
  • 在源函数中,将被提炼代码段替换为对目标函数的调用。
  • 编译,测试。

Inline Method(内联函数)

如果一个函数的本体与名称同样清楚易懂,那么在函数调用点插入函数本体,然后移除该函数。例如以下代码段:

int getRating(){
    return (moreThanFiveLateDeliveries())?2:1;
}
boolean moreThanFiveLateDeliveries(){
    return _numberOfLateDeliveries > 5;
}

可以修改为:

int getRating(){
    return (_numberOfLateDeliveries > 5)?2:1;
}

另外一种需要使用Inline Method的情况是:你手上有一堆组织不甚合理的函数,你可以把它们都内联到一个大型函数中,再从中提炼组织出合理的小型函数。内联函数常用的做法:

  • 检查函数,确定它不具有多态性。
  • 找出这个函数所有的调用点。
  • 将这个函数所有的调用点都替换为函数本体。
  • 编译测试。
  • 删除该函数的定义。
    【博主观点】如果由于代码架构分层导致的本体被抽离,请勿使用本手段进行代码重构。

Inline Temp(内联临时变量)

你有一个临时变量,只被一个简单的表达式赋值一次,而它妨碍了其他重构手法,那么可以将对该变量的引用动作,替换为表达式自身。例如:

double basePrice = anOrder.basePrice();
return (basePrice > 1000)

替换为

return (anOrder.basePrice() > 1000)

一般来说,只有临时变量妨碍了其他重构方法,你才需要进行内联化。内联临时变量的做法如下:

  • 检查临时变量的赋值语句,确保等号右边的表达式没有副作用。
  • 如果这个临时变量并未被声明为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 basePrice = _quantity * _itemPrice;
}

临时变量的问题在于它是临时的,并且只能在所属函数内使用。如果将临时变量替换为一个查询,那么同一个类中所有函数都能够调用到。常用的做法:

  • 找出只被赋值一次的临时变量(如果赋值多次考虑Split Temporary Varaiable分割成多个变量)
  • 临时变量右侧的表达式提炼到一个独立函数中
  • 编译测试
  • 在该临时变量上实施Inline Temp

Introduce Explaining Variable(引入解释性变量)

将复杂表达式(或者其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。

if((platform.toUpperCase().indexOf("MAC") > -1) &&
   (browser.toUpperCase().indexOf("IE") > -1) &&
    wasInitialized() && resize > 0)
{
    //do something
}

重构为

const bool isMacOs     = platform.toUpperCase().indexOf("MAC") > -1;
const bool isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
const bool wasResized = resize > 0; 

if (isMacOs && isIEBrowser && wasInitialized() && wasResized())
{
    //do something
}

非常复杂的表达式可能难以阅读,这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。这种重构方法与Extrat Method类似,但作者更推荐使用后者。常用的做法为:

  • 声明一个final型的临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它。
  • 将表达式中的“运算结果”这一部分,替换为上述的临时变量。
  • 编译,测试。
  • 重复上述过程,处理其它类似部分。

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);  

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;
}

由于传值和传址两种前提下,对参数赋值可能引起不同的结果。因此,在函数内对一参数进行赋值大大影响了函数的清晰度。一般的做法为:

  • 建立一个临时变量,把待处理的参数值赋予它。
  • 以“对参数的赋值动作”为界,将其后所有对此参数的引用点,全部替换为”对此临时变量的引用动作“。
  • 修改赋值语句,使其改为对新建之临时变量赋值。
  • 编译,测试。

Replace Method with Method Object(以函数对象取代函数)

如果你有一个大型函数,对其中的局部变量无法使用Extract Method。那么将这个函数放进一个单独对象中,局部变量转换为对象内的字段。那么可以在同一对象中将这个大型函数分解为多个小函数。
做法为:

  • 建立一个新类,根据待处理函数的用途,为这个类命名。
  • 在新类中建一个const字段,用以保存原先大函数所在的对象。我们将这个字段称为“源对象”。同时,针对原函数的每个临时变量和每个参数在新类中建立一个对应的字段保存之。
  • 在新类中建立一个构造函数,接收源对象及原函数的所有参数作为参数。
  • 在新类中建立一个compute()函数。
  • 将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。
  • 将旧函数的函数本体替换为这样一条语句:“创建上述新类的一个新对象,而后调用其中的compute函数”。

Substitute Algorithm(替换算法)

string FoundPerson(string[] people){
    foreach (var person in people){
        if (person == "Don"){
            return "Don";
        }if (person == "John"){
            return "John";
        }
    }
    return string.Empty;
}

重构为

String foundPerson(String[] people) {
    List candidates = Arrays.asList(new String[],{"Don", "John", "Kent"});
    for(int i = 0; i < people.length; i++)
       if(candidates.contains(people[i]))
          return people[i];
    return "";
}

解决问题的方法有很多种,采用更加清晰简洁的算法取代复杂的实现方式,一般的做法为:

  • 准备好一个更换的算法,让它通过编译。
  • 针对现有的测试,执行上述的新算法。
  • 若测试结果不同于原来的算法,进行检查和修正。

相关文章

网友评论

    本文标题:《重构》学习笔记(04)-- 重新组织函数

    本文链接:https://www.haomeiwen.com/subject/togxtctx.html