重构手法——重新组织函数

过长的函数往往包含了太多的信息,我们需要适时地整理函数。通常,整理函数的常见手法包含以下几个:

  1. 提炼函数(Extract Method);
  2. 内联函数(Inline Method);
  3. 内联临时变量(Inline Temp);
  4. 以查询取代临时变量(Replace Temp with Query);
  5. 引入解释性变量(Introduce Explaining Variable);
  6. 分解临时变量(Split Temp Variable);
  7. 移除对参数的赋值(Remove Assignments to Parameters);
  8. 以函数对象取代函数(Replace Method with Method Object);
  9. 替换算法(Substitute Algorithm);

这些手法并不是独立运用的,很可能在一次重构过程中,我们需要同时运用以上好几种手法。

一、移除对参数的赋值

什么是对参数赋值呢?

    public double addFruitPriceBefore(int appleCnt, int orangeCnt, double totalPrice){
        if(appleCnt > 10){
            totalPrice += appleCnt * APPLE_PRICE * 0.75;
        }
        if(orangeCnt > 12){
            totalPrice += orangeCnt * ORANGE_PRICE * 0.82;
        }
        return totalPrice;
    }

在以上的代码中,函数入参totalPrice在函数体内被赋值,这种现象就是对参数的赋值。

为什么需要对这种现象进行重构?
从代码可读性上来讲,这种现象降低了代码的清晰度,同时会让人混淆按值传递和按引用传递的概念,也会不利于其它重构手法的使用。

如何进行重构呢?
Step1:先在函数体内建立一个临时变量,将入参的值赋予这个临时变量;

    public double addFruitPriceBefore(int appleCnt, int orangeCnt, double totalPrice){
        double currentPrice = totalPrice;
        if(appleCnt > 10){
            totalPrice += appleCnt * APPLE_PRICE * 0.75;
        }
        if(orangeCnt > 12){
            totalPrice += orangeCnt * ORANGE_PRICE * 0.82;
        }
        return totalPrice;
    }

Step2:将其后所有对此入参的引用和赋值都替换为对临时变量的引用和赋值;

    public double addFruitPriceBefore(int appleCnt, int orangeCnt, double totalPrice){
        double currentPrice = totalPrice;
        if(appleCnt > 10){
            currentPrice += appleCnt * APPLE_PRICE * 0.75;
        }
        if(orangeCnt > 12){
            currentPrice += orangeCnt * ORANGE_PRICE * 0.82;
        }
        return currentPrice ;
    }

Step3:编译和测试。

二、引入解释性变量

我们为什么要使用这个手法呢?
先来看看如下一段代码,你能在短时间内理解它吗?

    public double getFruitPriceBefore(int appleCnt, int orangeCnt){
        return ((appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE) > 100
                ? (appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE)
                : (appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE) * 0.85)
                - (appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE) / 100;
    }

如上的代码,表达式非常复杂,难以理解。我们需要引入解释性的变量,让其变得更加容易理解,从而方便地进行下一步重构。
Step1:声明一个临时变量,将一部分表达式赋值给它;

    public double getFruitPriceAfter(int appleCnt, int orangeCnt){
        double basePrice = appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE;
        return ((appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE) > 100
                ? (appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE)
                : (appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE) * 0.85)
                - (appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE) / 100;
    }

Step2:使用临时变量替换复杂表达式中的一部分;

    public double getFruitPriceAfter(int appleCnt, int orangeCnt){
        double basePrice = appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE;
        return (basePrice  > 100
                ? basePrice 
                : basePrice  * 0.85)
                - basePrice  / 100;
    }

Step3:重复如上过程;

    public double getFruitPriceAfter(int appleCnt, int orangeCnt){
        double basePrice = appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE;
        double accountPrice = basePrice > 100 ? basePrice : basePrice * 0.85;
        double totalPrice = accountPrice - basePrice / 100;

        return totalPrice;
    }

三、分解临时变量

什么样的代码需要使用分解临时标量?

    public double getFruitPriceBefore(int appleCnt, int orangeCnt){
        double totalCost = APPLE_PRICE * appleCnt + ORANGE_PRICE * orangeCnt;
        double accountPrice = totalCost * 0.75;
        totalCost = accountPrice - totalCost / 100;
        return totalCost;
    }

在如上的代码中,临时变量totalCost被赋值超过一次,这意味着,它在函数中承担了一个以上的责任,这不利于代码的理解和接下来的重构,每个变量应该只承担一个责任。

如何操作呢?
Step1:在该临时变量声明和第一次被赋值的地方,修改其名称为新的临时变量;

    public double getFruitPriceBefore(int appleCnt, int orangeCnt){
        double basePrice= APPLE_PRICE * appleCnt + ORANGE_PRICE * orangeCnt;
        double accountPrice = totalCost * 0.75;
        totalCost = accountPrice - totalCost / 100;
        return totalCost;
    }

Step2:以该临时变量第二次赋值为界限,修改此前对该临时变量的引用为新的临时变量(包含当前表达式,因为在此之后,对原变量的引用想要的是第二次的赋值);

    public double getFruitPriceBefore(int appleCnt, int orangeCnt){
        double basePrice= APPLE_PRICE * appleCnt + ORANGE_PRICE * orangeCnt;
        double accountPrice = basePrice * 0.75;
        totalCost = accountPrice - basePrice / 100;
        return totalCost;
    }

Step3:在第二次赋值处,重新声明原先的临时变量;

    public double getFruitPriceBefore(int appleCnt, int orangeCnt){
        double basePrice= APPLE_PRICE * appleCnt + ORANGE_PRICE * orangeCnt;
        double accountPrice = basePrice * 0.75;
        double totalCost = accountPrice - basePrice / 100;
        return totalCost;
    }

四、内联临时变量

该重构手法往往是其它重构手法的中间步骤,作用是取消临时变量。

    public double getMaxFruitPriceBrfore(){
        double maxPrice = Double.max(APPLE_PRICE, ORANGE_PRICE);
        return (maxPrice > 5 ? 5 : maxPrice);
    }

在如上代码中,临时变量maxPrice显得很没必要,我们按照如下步骤去掉它。
Step1:找到该临时变量的所有引用点,将它们替换为该临时变量赋值的表达式;

    public double getMaxFruitPriceBrfore(){
        double maxPrice = Double.max(APPLE_PRICE, ORANGE_PRICE);
        return (Double.max(APPLE_PRICE, ORANGE_PRICE) > 5 ? 5 : Double.max(APPLE_PRICE, ORANGE_PRICE));
    }

Step2:修改完所有引用点后,去除该临时变量的声明及其赋值表达式;

    public double getMaxFruitPriceBrfore(){
        return (Double.max(APPLE_PRICE, ORANGE_PRICE) > 5 ? 5 : Double.max(APPLE_PRICE, ORANGE_PRICE));
    }

五、以查询取代临时变量

临时变量总是丑陋的,因为它们仅仅在当前函数作用域内有用,并总是驱使你写出更长的函数。要是能去掉所有的临时变量就好了。

    public double getFruitPriceBefore(int appleCnt, int orangeCnt){
        double appleCost = APPLE_PRICE * appleCnt * 0.85;
        double orangeCost = ORANGE_PRICE * orangeCnt * 0.77;

        if(appleCost > 50 || orangeCost > 30) {
            return 70;
        }
        return appleCost + orangeCost;
    }

如上代码中,有两个临时变量,我们使用如下的步骤来取消它们。
Step1:找出只被赋值过一次的临时变量,将它们的表达式提炼到一个独立的函数中;

    public double getFruitPriceAfter1(int appleCnt, int orangeCnt){
        double appleCost = getApplePrice(appleCnt);
        double orangeCost = getOrangePrice(orangeCnt);

        if(appleCost > 50 || orangeCost > 30) {
            return 70;
        }
        return appleCost + orangeCost;
    }

    private double getApplePrice(int appleCnt){
        return APPLE_PRICE * appleCnt * 0.85;
    }

    private double getOrangePrice(int orangeCnt){
        return ORANGE_PRICE * orangeCnt * 0.77;
    }

Step2:将临时变量内联到函数中引用它的地方;

    public double getFruitPriceAfter2(int appleCnt, int orangeCnt){
        if(getApplePrice(appleCnt) > 50 || getOrangePrice(orangeCnt) > 30) {
            return 70;
        }
        return getApplePrice(appleCnt) + getOrangePrice(orangeCnt);
    }

    private double getApplePrice(int appleCnt){
        return APPLE_PRICE * appleCnt * 0.85;
    }

    private double getOrangePrice(int orangeCnt){
        return ORANGE_PRICE * orangeCnt * 0.77;
    }

六、内联函数

有三种情形我们需要内联函数:

  1. 某些函数十分短小,其内部逻辑非常简单、清晰易读,且复用地也不多,此时不如把函数中的逻辑放回到调用它的地方。
    public double getMaxFruitPriceBrfore(){
        return returnMax();
    }
    private double returnMax(){
        return Double.max(APPLE_PRICE, ORANGE_PRICE);
    }

使用内联函数后,代码逻辑回归到调用它的地方:

    public double getMaxFruitPriceAfter(){
        return Double.max(APPLE_PRICE, ORANGE_PRICE);
    }
  1. 一次调用使用了太多的间接层,其中有些间接层是毫无价值的,那么就可以使用内联函数的方法,去除这些中间层。

  2. 代码中有一群组织不是很合理的函数,我们需要将这些函数都内联到它们的调用处,然后再重新组织新的函数,或者整体搬移它们。

    public void printFruitPriceBefore(){
        printApplePrice();
        printOrangePrice();
    }

    private void printApplePrice(){
        System.out.println("您购买了apple");
        System.out.println("orange非常甜");
    }

    private void printOrangePrice(){
        System.out.println("您购买了orange");
        System.out.println("apple非常脆");
    }

此处两个子函数中的代码组织地不合理,我们先把它们都内联到调用它们的地方:

    public void printFruitPriceAfter(){
        System.out.println("您购买了apple");
        System.out.println("orange非常甜");
        System.out.println("您购买了orange");
        System.out.println("apple非常脆");
    }

然后再重新组织一下:

    public void printFruitPriceBefore(){
        printApplePrice();
        printOrangePrice();
    }

    private void printApplePrice(){
        System.out.println("您购买了apple");
        System.out.println("apple非常脆");
    }

    private void printOrangePrice(){
        System.out.println("您购买了orange");
        System.out.println("orange非常甜");
    }

七、提炼函数

当我们看到一个很长的函数,其中有多块代码需要用注释来标明它们的作用,那么我们就可以将这部分代码提炼为多个单独的函数,然后在原函数中调用它们。

    public double getFruitPriceBefore(){
        // 付款
        System.out.println("付款:" + (APPLE_PRICE + ORANGE_PRICE) + "元.");

        // 打印总价
        System.out.println("*********");
        System.out.println("欢迎下次光临!");
        System.out.println("*********");

        return APPLE_PRICE + ORANGE_PRICE;
    }

提炼函数后:

    public double getFruitPriceAfter(){
        // 付款
        System.out.println("付款:" + (APPLE_PRICE + ORANGE_PRICE) + "元.");

        printFruitPrice();

        return APPLE_PRICE + ORANGE_PRICE;
    }

    private void printFruitPrice(){
        System.out.println("*********");
        System.out.println("欢迎下次光临!");
        System.out.println("*********");
    }

然而,大多数需要提炼的场景并没有这么简单,更多的会遭遇局部变量的场景:

  1. 存在局部变量,但只是读取并不修改局部变量,比如下例中的appleCnt、orangeCnt,我们只需将它们作为提炼函数的参数传递过去即可;
  2. 存在局部变量,并且对局部变量进行再次赋值,但是没有其它地方使用了,比如下例中的appleCost、orangeCost,此时我们可以将这些被赋值的局部变量移动到提炼函数中进行声明和赋值;
  3. 存在局部变量,并且对局部变量进行再次赋值,且该局部变量在下面还有其它代码使用,比如下例中的fruitCost,我们就需要在提炼函数中返回该局部变量;
    public Double getFruitPriceBefore2(Integer appleCnt, Integer orangeCnt){
        // 计算总价
        double appleCost = appleCnt * APPLE_PRICE;
        double orangeCost = orangeCnt * ORANGE_PRICE;
        double fruitCost = appleCost + orangeCost;

        // 打印总价
        System.out.println("*********");
        System.out.println("You cost :" + fruitCost + "yuan.");
        System.out.println("*********");

        return fruitCost;
    }

重构后

    public double getFruitPriceAfter2(int appleCnt, int orangeCnt){
        double fruitCost = calculateFruitPrice(appleCnt, orangeCnt);

        printFruitPrice(fruitCost);

        return fruitCost;
    }

    private double calculateFruitPrice(int appleCnt, int orangeCnt){
        double appleCost = appleCnt * APPLE_PRICE;
        double orangeCost = orangeCnt * ORANGE_PRICE;
        return appleCost + orangeCost;
    }

    private void printFruitPrice(double fruitCost){
        System.out.println("*********");
        System.out.println("You cost :" + fruitCost + "yuan.");
        System.out.println("*********");
    }

八、以函数对象取代函数

当某个过长的函数中,局部变量泛滥成灾,我们压根就无法理清楚各个局部变量与代码逻辑之间的关系,很难提炼出单独的函数,那么就可以使用终极大杀器——以函数对象取代函数。

    double getFruitPriceBefore(int appleCnt, int orangeCnt){
        double basePrice = appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE;
        double accountPrice = basePrice > 100 ? basePrice * 0.85 : basePrice;
        double totalPrice = accountPrice - basePrice / 100;
        return totalPrice;
    }

    void printFruitPrice(){
        System.out.println("*********");
        System.out.println("欢迎下次光临!");
        System.out.println("*********");
    }

假设如上这段代码局部变量太多了,我们想使用以函数对象取代函数的重构手法,那么我们就新建一个类(对象),来替换当前的函数。

如此,原先的局部变量都变成了对象的字段,我们在原函数中实例化一个新类的构造函数,并初始化这些对象字段,如果需要调用原函数中的其它函数,还需要将原函数的对象引用传递给新的类。

    double getFruitPriceAfter(int appleCnt, int orangeCnt){
        return new PriceGenerator(this, appleCnt, orangeCnt).generatorPrice();
    }

    void printFruitPrice(){
        System.out.println("*********");
        System.out.println("欢迎下次光临!");
        System.out.println("*********");
    }
public class PriceGenerator {
    private static final Double APPLE_PRICE = 6.5;
    private static final Double ORANGE_PRICE = 3.4;

    private ReplaceMethodWithMethodObject rq;
    private double basePrice;
    private double accountPrice;

    PriceGenerator(ReplaceMethodWithMethodObject rq, double appleCnt, double orangeCnt){
        this.rq = rq;
        this.basePrice = appleCnt * APPLE_PRICE + orangeCnt * ORANGE_PRICE;
        this.accountPrice = basePrice > 100 ? basePrice * 0.85 : basePrice;
    }

    double generatorPrice(){
        rq.printFruitPrice();
        return accountPrice - basePrice / 100;
    }

}

九、替换算法

如果你发现做一件事可以有更加清晰简便的方式,那就应该以这种新的方式来取代原先复杂的方式。

虽然重构是将一些复杂的逻辑分解为简单的小块,但有时候代码逻辑就是很难重构,你不得不将整个逻辑全部替换掉。

PS:
使用任何重构手法之前,你都必须构筑自己的测试体系,任何一步重构的施展都离不开测试的支持。

你可能感兴趣的:(重构手法——重新组织函数)