【《重构 改善既有代码的设计》学习笔记6】重新组织函数

本篇文章的内容来自《重构 改善既有代码的设计》一书学习笔记整理并且加上自己的浅显的思考总结!

重构手法中,很大一部分是对函数进行整理,使之更恰当地包装代码。

重新组织函数

对过长的函数进行拆解,提炼函数,并处理局部变量,使得拆解后的函数更加清晰并且能够更好的工作。

1、提炼函数(Extract Method)

概要

你有一段代码可以被组织在一起并独立起来。 将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。

void print(){
    printBanner();
    System.out.print("name:"+ _name);
    System.out.print("age:"+ age);
}
// 修改后
void print(){
    printBanner();
    printDetail();
}
void printDetail(){
     System.out.print("name:"+ _name);
     System.out.print("age:"+ age);
}

动机

提炼函数是最常用的手法之一。将一段独立的代码进行抽离并放入一个独立的函数中,并给这个函数起一个简短而命名良好的名字,这种提炼也可以方便后续的代码的复用。

一个函数多长才算合适?

长度不是问题,关键在于函数名称和函数本体之间的语义距离。

做法

♢创建一个函数,根据这个函数的意图来对它命名。(“做什么”命名,而不是“怎么做”命名)

♢将提炼出的代码从源函数复制新建目标函数中

♢检查是否有需要处理的临时变量

♢处理完成,编译

♢在源函数中,调用提炼的目标函数,编译、测试

【这个重构使用较多,希望一定要能够掌握】

范例

  • 无局部变量

比较简单,剪切、粘贴,插入一个函数即可。

无局部变量

  • 有局部变量

局部变量:包括传入源函数的参数和源函数所申明的临时变量。 **局部变量的作用域仅限于 源函数。**所以需要额外去处理这些变量。

局部变量最简单的情况:被提炼的函数段只是读取这些变量的值,并不修改它们。这种情况可以简单传参数到目标函数。

有局部变量

如果局部变量是个对象,也可以进行参数传递。但是一定要注意,目标函数是否会对对象赋值.

【对象当作参数传递,要了解按值传递和按引用传递的概念和相关的知识】

  • 对局部变量在赋值

♢ 变量只是单纯的初始值,可以在新函数中进行初始化。

♢ 变量如果有其他处理,必须要将它的值作为参数传给目标函数。

对局部变量在赋值

如果需要返回的变量不止一个,又该怎么办?

(1) 、提炼另一块代码,每个函数只返回一个值。要安排多个函数,用以返回多个值。

(2)、使用传递对象的方式进行

2、内联函数(Inline Method)

概要

一个函数的本体与名称同样清晰易懂。在函数调用点插入函数本体,然后移除该函数。

【减少函数】

int getRating(){
    return (getLargeSize()) ? 2 : 1 ;
}
boolean getLargeSize(){
    return _largeSize > 5;
}
// 修改后
int getRating(){
    return (_largeSize > 5) ? 2 : 1 ;
}

动机

使用内部代码和函数名称同样清晰易懂,此时就应该去掉这个函数。间接性可能带来帮助,但是非必要的间接性总是让人不舒服。

太多的间接层,使得系统中所有的函数似乎只是对另一个函数的简单委托。使用内联手法,找出那些有用的间接层,同时去掉无用的间接层。

做法

♢ 检查函数,确定它不具有多态性(如果子类继承了这个函数,就不要将此函数内联,因为子类无法复写一个不存在的函数)【此问题目前IDE就可以帮助检查】

♢ 找出函数的所有的被调用点

♢ 将这个函数的所有被调用点替换为函数本体

♢ 编译、测试

♢删除该函数的定义

【内联函数 在真是的使用中可能不是那么容易,就如上面概要中的那个代码例子】,如果有十个函数使用getLargeSize(),然后将这个函数使用内联,那么带来一个问题,要修改判断条件 ,使用_largeSize > 10 ,那就要修改内联后所有函数,而不是只修改一个地方了。

使用的时候还是要仔细判断,谨慎选择。

3、内联临时变量(Inline Temp)

概要

你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。

将所有对该变量的引用动作,替换为对它赋值的那个表达式

double basePrice = order.basePrice();
return (basePrice > 100)

// 修改后
return (order.basePrice() > 100)

动机

唯一单独使用内联临时变量情况是:发现某个临时变量被赋予某个函数调用的返回值。 一般来说,如果临时变量不妨碍其他重构手法,留在那儿就行,妨碍了(如影响提炼函数)就应该将它内联化。

做法

♢ 检查该临时变量时候真的只被 赋值一次。

【我一般在代码中如果一个临时变量的赋值表达式多次使用,我会定义一个临时变量,其他地方使用这个临时变量】,例如:

void getPrice(){
    if(order.basePrice()){
        // do something
    }
    System.out.print("basePrice :" + order.basePrice());
    //这个函数 还有地方也使用 order.basePrice()
        
}
// 修改后
void getPrice(){
    double basePrice = order.basePrice();
    if(basePrice){
         // do something
    }
    System.out.print("basePrice :" + basePrice);
     //这个函数 还有地方也使用 basePrice
}

4、以查询取代临时变量(Replace Temp with Query)

概要

你的程序以一个临时变量保存某一次的运算结果。将这个表达式提炼到一个独立的函数中,将这个临时变量的所有的引用点替换为新函数的调用。

double basePrice = _quantity * _itemPrice;
if(basePrice > 100){
    return basePrice * 0.95;
}else{
    return basePrice * 0.98;
}

// 修改后
if(basePrice() > 100){
    return basePrice() * 0.95;
}else{
    return basePrice() * 0.98;
}
double basePrice(){
    return _quantity * _itemPrice;
}

可以思考一下这样的一个操作有什么好处?

【此重构手法和内联函数的重构手法进行对比,思考这些重构手法什么场景使用!】

动机

临时变量的问题在于:它们都是暂时的,而且只能在所属函数内部使用。 如果把临时变量替换为一个查询,那么同一个类都将可以访问获取这份信息。使得类编写更清晰的代码。

做法

♢ 找出只被赋值一次的临时变量。

♢ 将该临时变量申明为final (确保没有地方去修改这个临时变量)

上面例子的代码,不知道你是否仔细思考,这样的改动可以会带来性能问题,这里先不考虑它造成的性能的问题。

范例

查询取代临时变量

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

概述

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

if(platform.toUpperCase().indexOf("MAC") > -1 &&
   browser,toUpperCase().indexOf("IF") > -1 &&
   wasInitialized() && resize > 0) {
    // do something
}
// 修改后

final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrower = browser,toUpperCase().indexOf("IF") > -1;
final boolean wasResized = resize > 0;

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

动机

表达式有可能非常复杂而难以阅读。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。

【阿里巴巴的java规范插件会有相应的校验提示,建议java开发的同学都装一个阿里巴巴Java开发规约插件p3c】

在条件逻辑中,引用解释性变量特别有价值: 将每个复杂的条件字句提炼出来,以一个良好命名的临时变量来解释对应的条件字句。

如果要解释一段代码的意义,尽量使用 提炼函数。当局部变量使 提炼函数 难以进行的时候,可以使用 引用解释型变量

做法

声明一个final临时变量 ,将待分解复杂表述式的一部分动作的运算结果赋值给它。

♢ 将表达式 中的 “运算结果” 替换为临时变量

♢ 编译、测试

范例

  • 引入解释型变量 范例

引入解释型变量 范例

  • 提炼函数 范例

提炼函数 范例

上面这种情况,两种重构手法都可以使用,那么到底应该在什么时候使用 引用解释型变量 呢?

在 引入 提炼函数 需要花费更大工作量时。也即 如果要处理的是一个拥有大量局部变量的算法,使用提炼函数 绝非易事。

6、分解临时变量(Split Temporary Varibale)

概要

程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。

针对每次赋值,创造一个独立、对应的临时变量。

double temp = 2 * (height + width);
System.out.print(temp);
temp = height * width;
System.out.print(temp);
// 修改为
final double perimeter = 2 * (height + width);
System.out.print(perimeter);
final double area = height * width;
System.out.print(area);

动机

临时变量有各种不同的用途,其中某些用途很自然地导致临时变量被多次赋值。 除 “循环变量”和“结果收集变量([i = i +1],i即为结果赋值变量)”这两种情况外,很多临时变量 用于保存一段冗长的代码的运算结果,以便稍后使用,这种临时变量应该只被赋值一次。 同一个临时变量承担两件不同的事情,会令代码阅读者糊涂。

做法

♢在待分解临时变量的声明及其第一次赋值处,修改其名称。

♢将新的临时变量声明为final

♢在改临时变量第二次赋值的时候在修改,按照首次的套路

♢ 重复上述,编译、测试

范例

分解临时变量 范例

7、移除对参数的赋值(Remove Assignmets to Parameters)

概要

对一个参数进行赋值。以一个临时变量取代该参数的位置。

int discount (int inputVal, int quantity ,int yearToDate){
    if(inputVal > 50){
        return inputVal -= 2;
    }
}
// 修改后

int discount (int inputVal, int quantity ,int yearToDate){
    int result = inputVal;
    if(inputVal > 50){
        return result -= 2;
    }
}

动机

除非你对“对参数赋值” 非常清楚,也即你非常清楚 java的按值传递和按引用传递。

【在真实的项目代码,很多业务中,一个对象被当作参数在几个方法处理和修改】

做法

♢ 建立一个临时变量,把待处理的参数值赋予它。

♢修改其后所有的引用点为临时变量

♢ 编译、测试

使用的时候一定要注意 按引用传递 参数情况

范例

移除对参数的赋值 范例

按值传递和按引用传递 : 如果理解底层的话本质都是按值传! 基本类型拷贝原值,引用类型是拷贝引用的地址(也是值)!

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

概要

你又一个大型的函数,其中对局部变量的使用使得你无法采用 提炼函数

将这个函数放进一个单独的对象中,如此依赖局部变量就成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小函数。

以函数对象取代函数

动机

本书不断强调 小型函数的优美动人。只要将相对独立的代码从大型函数中提炼出来,就可以大大提高代码的可读性。

做法

♢ 新建一个类,根据处理函数用途命名这个类。

♢ 在新类中创建字段和源函数中每个临时变量对应

♢ 新类中建立构造函数,接受原函数所有参数

♢在新类中建立 compute() 函数,将源函数的代码拷贝到compute()中

♢ 编译、测试

范例

以函数对象取代函数 范例

9、替换算法(Substitute Algorithm)

概要

你想要某个算法替换为另一个更清晰的算法。 将函数本体替换为另一个算法。

String foundPerson(String[] people){
    if(int i = 0; i < people.length; i++){
        if("Don".equals(people[i])){
            return "Don";
        }
       if("Jack".equals(people[i])){
            return "Jack";
        }
        //.....
    }
}
// 修改后
String foundPerson(String[] people){
    List candidates = Arrays.asList(new String {"Don","Jack"});
    if(int i = 0; i < people.length; i++){
        if(candidates.contains(people[i])){
            return people[i];
        }
    }
}


动机

解决一个问题一般会有好几种方法,某些方法会比另一些简单。算法也是如此。找到一个更清晰的方式取得复杂的方式。

替换一个巨大而且赋值的算法是非常困难的,只有将它分解为较简单的小型函数,然后在进行算法的替换工作。

做法

♢ 准备好新的算法,替换之前旧的算法,编译、测试

对于每个测试用例,分别以新旧两种算法执行,观察结果是否相同,这可以帮助你看到哪一个测试用例 出现麻烦,以及出现怎么样的麻烦。

总结

本章学习了函数的重构,很多都是和日常开发紧密相关的,从做一件小事的细节看出一个人的品质,从小的代码编写细节,看出一个coder的功底。代码的编写可以持续的精进,从小的细节写出好的代码,脚踏实地,不要好高骛远,眼高手低。做一个好的coder,做好自己的工作。

笔记中有很多重构手法的做法没有书中全,我是按照自己的理解进行整理,如需看详细内容请阅读本书第六章


如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到,谢谢!

如果帅气(美丽)、睿智(聪颖),和我一样简单善良的你看到本篇博文中存在问题,请指出,我虚心接受你让我成长的批评,谢谢阅读!
祝你今天开心愉快!


欢迎访问我的csdn博客和关注的个人微信公众号!

愿你我在人生的路上能都变成最好的自己,能够成为一个独挡一面的人。

不管做什么,只要坚持下去就会看到不一样!在路上,不卑不亢!

博客首页 : http://blog.csdn.net/u010648555

© 每天都在变得更好的阿飞

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页