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

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

  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:
使用任何重构手法之前,你都必须构筑自己的测试体系,任何一步重构的施展都离不开测试的支持。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容