zoukankan      html  css  js  c++  java
  • 第6章 重新组织函数

    简介:

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

        重构的很多问题其实都来自“过长函数(Long Methods)”,要重构它是因为它往往包含过多的信息,这些信息又被他错综复杂的逻辑掩盖,不易鉴别。

        解决过长函数的重构方法,其中一个是“提炼函数(Extract Method)”,它把一段代码从源函数中提炼出来,放入一个单独的函数中;或者就是使用“内联函数(Inline Method)”,它将一个函数调用替换为函数本体,这样替换是因为提炼的函数没有做任何实质性的事情。

        提炼函数难点是:处理局部变量,如临时变量,参数等。

        提炼函数处理函数替换临时变量的一个方法:“以查询取代临时变量”处理参数的方法:“移除对参数的赋值”。

    Extract Method(提炼函数)

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

    2.动机:

    (1)如果每个函数的粒度都很小,那么函数被复用的机会更大。

    (2)其次,这会使高层函数读起来像注释(小型函数要很好地命名)。

    (3)最后,如果函数都是细粒度,那么函数的覆写也会更容易些。

    (ps:函数命名长度不是问题,重要的是函数名称和函数本体之间的语义距离。)

    3.做法:

    (1)创造一个新函数,根据函数意图对它命名(即使你提炼的代码很简单,如只是一条消息或一个函数调用,只要新的函数名称能够更好昭示代码意图,就应该提炼)。

    (2)将提炼出的代码从源函数复制到目标函数。

    (3)检查提炼出的代码,看是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。

    (4)检查是否有“仅用于被提炼代码段”的临时变量,如果有,在目标函数中将它们声明为临时变量。

    (5)检查提炼代码段,看是否有任何局部变量的值被它改变。(疑问1:这里不太理解,是指提炼代码改变了源函数的局部变量值么?

    (6)将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。

    (7)处理完所有局部变量后,再进行编译。

    (8)源函数中,将被提炼代码段替换为对目标函数的调用。

    (9)编译,测试。

    4.小结:

    (1)将能提出的尽量都提出来。

    (2)提出的代码如果希望返回两个值,就挑选另一块代码来提炼,最好让每个函数只有一个返回值。

    Inline Method(内联函数)

    1.概念:一个函数的本体与名称同样通俗易懂。于是在函数调用点插入函数本体,然后移除该函数。

    举个例子,原函数:

    public class ExtractMethod {
        int number = 6;
    
        public int getRating() {
            return (moreThanFiveLateDeliveries()) ? 2 : 1;
        }
    
        private boolean moreThanFiveLateDeliveries() {
            return number > 5;
        }
    }

    重构后函数:

    public class ExtractMethod {
        int number = 6;
    
        public int getRating() {
            return (number > 5) ? 2 : 1;
        }
    }

    2.动机:

    (1)当遇到其内部代码与函数名称同样清晰易读,即去掉这种无用的间接层,留下有用的间接层。

    (2)当你手上有一群组织不甚合理的函数,就可以先将它们内联到一个大型函数,然后再提炼出合理地小型函数。

     3.做法:

    (1)检查函数,确定它不具有多态性(如果有子类继承了这个函数,就不要将这个函数内敛,不然子类无法覆写一个不存在的函数)。

    (2)找出这个函数所有的被调用点。

    (3)将这个函数所有的被调用点都替换为函数本体。

    (4)编译,测试。

    (5)删除该函数的定义。

    Inline Temp(内联临时变量)

    1.概念:

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

    //改前
    BigDecimal applePrice = getPrice().multiply(BigDecimal.valueOf(100L));
    return (applePrice > 100);
    
    //改后
    return (getPrice().multiply(BigDecimal.valueOf(100L)) > 100);

    2.动机:

    如果一个临时变量妨碍了其他重构手法,就应该将它内联化。

    3.做法:

    (1)检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。

    (2)如果这个临时变量未被声明为final,则先声明为final,然后编译(通过看是否能编译通过,来确认这个临时变量只被赋值过一次)。

    (3)找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。

    (4)每次修改后,编译并测试。

    (5)修改完所有引用点后,删除该临时变量的声明和赋值语句。

    (6)编译,测试。

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

    1.概念:

    如果程序以一个临时变量保存某一表达式运算结果,则将这个表达式提炼到单独函数中,将所有对临时变量的引用点替换为对新函数的调用,此后,新函数就可被其他函数使用。

    //改前
    double applePrice = getPrice() * basePrice;
    if (applePrice > 100) {
        return applePrice * 0.95;
    } else {
        return applePrice * 0.98
    }
    
    //改后
    if  (applePrice() > 100) {
        return applePrice * 0.95;
    } else {
        return applePrice * 0.98
    }
    
    private double applePrice() {
        getPrice() * basePrice;
    }

    2.动机:

      2.1临时变量问题在于:

    (1)是暂时的,只能在函数内使用

    (2)可能会写出很长的函数表达式

      2.2改成函数后:

    (1)可供其他函数调用

    (2)使代码可读性强

    3.做法:

    用例子看:

    //原始代码
    public double getPrice() {
    int basePrice = quantity * itemPrice; double discountFactor; if (basePrice > 1000) { discountFactor = 0.95; } else { discountFactor = 0.98; } return basePrice * discountFactor; }

    (1)找出赋值一次的临时变量,然后先用final去定义它们,去检查是否真的只被赋值了一次,如果编译出错说明不止被赋值了一次,就不该进行这项重构:

    public double getPrice() {final int basePrice = quantity * itemPrice;
        final double discountFactor;
        if (basePrice > 1000) {
            discountFactor = 0.95;
        } else {
            discountFactor = 0.98;
        }
        return basePrice * discountFactor;
    }

    (2)每次提取一个临时变量的函数,编译通过后再进行下一个:

    public double getPrice() {
        final int basePrice = getBasePrice();
        final double discountFactor;
        if (basePrice > 1000) {
            discountFactor = 0.95;
        } else {
            discountFactor = 0.98;
        }
        return basePrice * discountFactor;
    }
    
    private int getBasePrice() {
        return quantity * itemPrice;
    }

    (3)编译测试完后,再依次替换所有的引用:

    public double getPrice() {
        final int basePrice = getBasePrice();
        final double discountFactor;
        if (getBasePrice() > 1000) {
            discountFactor = 0.95;
        } else {
            discountFactor = 0.98;
        }
        return getBasePrice() * discountFactor;
    }
    
    private int getBasePrice() {
        return quantity * itemPrice;
    }

    (4)然后再提炼下一个临时变量:

    public double getPrice() {
        final int basePrice = getBasePrice();
        final double discountFactor = getDiscountFactor;
        return getBasePrice() * discountFactor;
    }
    
    private int getBasePrice() {
        return quantity * itemPrice;
    }
    
    private double getDiscountFactor() {
        if (getBasePrice() > 1000) {
            discountFactor = 0.95;
        } else {
            discountFactor = 0.98;
        }
    }

    (5)删去final定义的变量,最后再进行一次编译测试:

    public double getPrice() {
        return getBasePrice() * getDiscountFactor();
    }
    
    private int getBasePrice() {
        return quantity * itemPrice;
    }
    
    private double getDiscountFactor() {
        if (getBasePrice() > 1000) {
            discountFactor = 0.95;
        } else {
            discountFactor = 0.98;
        }
    }

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

    1.概念:

    有一个复杂的表达式,将该复杂表达式的结果放进一个临时变量,以此表达式名称解释表达式用途。

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

    //改变后

    final boolean isMacOs = platform.toUpperCase.indexOf("MAX") > -1;
    final boolean isIEBrowser = browser.toUpperCase.indexOf("IE") > -1;
    final boolean wasResized = resize > 0;
    if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
    //do something
    }

    2.动机:

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

    3.做法:

    //改之前
    public
    double privce() { //price is base price - quantity discount + shipping return quantity * itemPrice - Math.max(0, quantity - 500) * itemPrice * 0.05 + Math.min(quantity * itemPrice * 0.1, 100.0); }

    (1)声明一个final临时变量,将待分解表达式中一部分的运算当做结果赋值给它,并替换临时变量:

    public double privce() {
        //price is base price - quantity discount + shipping
        final double basePrice = quantity * itemPrice;
        return basePrice - Math.max(0, quantity - 500) * itemPrice * 0.05 + Math.min(quantity * itemPrice * 0.1, 100.0);
    }

    (2)依次提出:

    public double privce() {
        //price is base price - quantity discount + shipping
        final double basePrice = quantity * itemPrice;
        final double quantityDiscount = Math.max(0, quantity - 500) * itemPrice * 0.05;
        final double shipping = Math.min(quantity * itemPrice * 0.1, 100.0);
        return basePrice - quantityDiscount + shipping;
    }

    (*)运用提炼函数来试着处理:

    public double privce() {
        return getBasePrice() - getQuantityDiscount() + getShipping();
    }
    
    private double getBasePrice() {
        return quantity * itemPrice;
    }
    
    private double getQuantityDiscount() {
        return Math.max(0, quantity - 500) * itemPrice * 0.05;
    }
    
    private double getShipping() {
        return Math.min(quantity * itemPrice * 0.1, 100.0);
    }

    问:到底什么时候用引入解释性变量的方式,什么时候用提炼函数的方式呢?

    答:该重构方法主要是在提炼函数需要花费更大工作量时才使用。比如你有一个拥有大量局部变量的算法,那么使用提炼函数绝非易事。这时候就可以使用本文的方法来整理代码,然后再考虑下一步

    怎么办;一旦搞清楚代码逻辑后,就可以运用以查询取代临时变量把中间引入的那些临时变量去掉。我想你会比较喜欢提炼函数,因为对于同一对象的任何部分,都可以根据自己的需要取用这些提炼

    出来的函数。一开始会把这些新函数声明为private;如果其它对象也需要它们,就可以轻易释放这些函数的访问控制。

    Split Temporary Variable(分解临时变量)

    1.概念:

    你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被利用于搜集计算结果。针对每次赋值,创造一个独立的,对应的临时变量。

    //原始代码
    double
    temp = 2 * (height + weight); log.info("temp:{}", temp); temp = height * width; log.info("temp:{}", temp);
    //改后
    final double perimeter = 2 * (height + weight);
    log.info("perimeter:{}", perimeter);
    final double area = height * width;
    log.info("area:{}", area);

    2.动机:

    当一个临时变量被赋值多次,就意味着它承担了一个以上的职责,就会降低可读性,因此就应该被替换成多个临时变量。

    3.做法:

    //改变前,可以看出acc被赋值两次
    public double getDistanceTravelled(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;
    }

    (1)将赋值两次的变量依次替换:

    public double getDistanceTravelled(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;
    }

    (2)然后再用其他手法进行重构

    Remove Assignments to Paramenters(移除对参数的赋值)

    1.概念:

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

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

    2.动机:

    如例子所示,代码中改变了传入参数inputVal的值,降低了代码清晰度,混用了按值传递和按引用传递这两种参数传递方式。

    3.做法:

    (1)新建一个临时变量,把待处理的参数值附给它。

    (2)然后将代码后面所有对参数的引用替换为对新建临时变量的引用。

    (3)修改赋值语句,使其改为对新建临时变量的赋值。

    (4)编译,测试。

    (ps:较长函数中可以使用final来看引用的次数,提高代码清晰度,但在短的或者看起来很清楚的代码中,没有必要用)

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

    1.概念:

    当有一个大型函数,其中对局部变量的使用使你无法采取提炼函数,于是将这个函数放进单独对象中,如此一来局部变量就成了对象内的字段,然后你可以在同一个对象中将这个大型函数分解为多个小型函数。 

    2.动机:

    当一个函数中局部变量泛滥,想分解这个函数是非常困难的,那么使用以函数对象取代函数这个方法,可以减轻这个负担。它会将所有局部变量都变成函数对象的字段,然后就能对新对象通过提炼函数创造新的函数,从而将原本的大型函数拆解变短。

    3.做法:

    原函数:

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

    (1)建立一个新类,根据待处理函数用途,为这个类命名。

    class Gamm{}

    (2)在新类中建立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;
    ....
    }

    (3)在新类中创建一个构造函数,接收源对象及原函数的所有参数作为参数。

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

    (4)在新类中建立一个compute()函数。

    (5)将原函数的代码复制到compute()函数中,如果需要调用原函数的任何函数,请通过源对象字段调用。

    int compute(){
        importValue1 = (value * quantity) + _account.delta();
        importValue2 = (value * year2Date) + 200;
        if(year2Date - importValue1 >200)
            importValue2-=50;
        importValue3 = importValue2 * 8;
        //.....        
        return importValue3 - 2 * importValue1;
    }

    (6)编译

    (7)将旧函数的函数本体替换成这样一句话,创建上述新类的一个新对象,而后调用其中的compute()函数。

    int gamm(int value, int quantity, int year2Date){
        return new Gamm(this,value,quantity,year2Date).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;
    }

    小结:将大型函数中,或者变量特别多的函数,将它们拆分成小的函数,可以轻松地对compute()函数采取提炼函数,且不必担心参数传递的问题。

    Substitute Algorithm(替换算法)

    1.概念:你想要把某个算法替换为另一个更加清晰的算法,即将函数本体替换成另一个算法。

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

    小结

    在满足一定情况下:

    1.函数过长则重构,重构想到提炼函数的方法(提炼函数)。

    2.直接用方法本身(内联函数)。

    3.用自身表达式替换当前引用(内敛临时变量,如直接return)。

    4.用方法替换某一表达式(以查询取代临时变量)

    5.将复杂表达式放进临时变量(引入解释性变量)

    6.将一个赋值多次的临时变量替换成不同的变量(分解临时变量)

    7.传入参数要赋值,就用另一个临时变量代替(移除对参数的赋值)

    8.大型函数局部变量泛滥,将这部分单独拆出去(以函数对象取代函数)

    9.将看起来不太聪明的算法替换成看起来简洁干净的算法(替换算法)

  • 相关阅读:
    阿里云高级技术专家周晶:基于融合与协同的边缘云原生体系实践
    Spring Boot Serverless 实战系列“架构篇” 首发 | 光速入门函数计算
    基于 EMR OLAP 的开源实时数仓解决方案之 ClickHouse 事务实现
    【ClickHouse 技术系列】 在 ClickHouse 中处理实时更新
    LeetCode_Two Sum
    LeetCode_ Remove Element
    LeetCode_Same Tree
    LeetCode_Symmetric Tree
    LeetCode_Path Sum
    LeetCode_Merge Sorted Array
  • 原文地址:https://www.cnblogs.com/wencheng9012/p/13519515.html
Copyright © 2011-2022 走看看