重构,改善既有代码的设计(理论篇)

256 阅读20分钟

文章目录


前言

本文是笔者毕业后的第一篇blog,将从三个方面讨论代码重构。即:1.代码重构是什么;2.常用的重构手法;3.代码中的“坏味道”。本文的姊妹篇是程序员必学的代码重构(实战篇)
本篇blog是《重构,改善代码既有代码的设计》(密码: ab5g)一文的读书笔记,读书笔记与书一起食用效果更佳哦。欢迎点赞、收藏、评论三连~,谢谢大家。

重构是什么?

谈谈定义

重构是软件内部结构的一种调整,在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。敲重点:1. 使软件更容易被理解和修改;2. 不改变软件外部行为,对外部用户/程序员不感知。

何时重构?

满足以下原则考虑重构:1. 三次原则:“事不过三,三则重构”(反感此处代码的修改,容忍不超过三次);2. 添加功能时重构;3. 修补bug时重构;4. Review代码时重构。

常用重构手法

常用的重构手法将以总结、具体做法、代码示例三个步骤来进行说明。

提炼函数(Extract Method)

将该段代码放进一个独立函数中,并让函数名称解释该函数的用途。过长函数需要注释才能让人理解,此时应抽出独立函数,使之易复用;易读;易复写。
具体做法:

1. 无局部变量:简单复制、粘贴到被提炼函数即可;
2. 有局部变量但是被提炼代码段只读取该变量,不修改:传参给被提炼函数即可;
3. 有局部变量且再赋值:
 3.1. 该变量只在被提炼代码段使用:将临时变量声明一并移入被提炼代码段,一起提炼出去;
 3.2. 该变量在被提炼代码段之外也有使用:让提炼函数返回该变量改变后的值。有人会问:如果返回变量不止一个,怎么办?安排多函数返回多个值or挑选另一块代码来提炼,每个函数只返回一个值。使用Replace Temp with Query减少临时变量。

代码示例:

public class ExtractMethod {
    /**
     * 重构前
     */
    void printOwing(double amount) {
        printBanner();
        //print Details
        System.out.println("name" + name);
        System.out.println("amount" + amount);
    }

    /**
     * 重构后
     */
    void printOwingRefactor(double amount) {
        printBanner();
        printDetails(amount);
    }

    private void printDetails(double amount) {
        System.out.println("name" + name);
        System.out.println("amount" + amount);
    }
}

以查询取代临时元素(Replace Temp with Query)

将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用,那么新函数可被其他函数使用。
具体做法:

1. 找出被赋值一次的临时变量(如果临时变量被赋值超过多次,使用Split Temporary Variable将其分割为多个变量);
2. 将临时变量修改成final3. 将“临时变量赋值”等号右侧部分提炼到一个独立函数中 ;
4. 最后将变量替换为方法,去掉final语句。

代码示例:

public class ReplaceTempWithQuery {
    /**
     * 重构前
     * @return
     */
    double printOwing() {
        double basePrice = quality * itemPrice;
        if (basePrice > 1000) {
            return basePrice * 0.95;
        } else {
            return basePrice * 0.98;
        }
    }
    /**
     * 重构后
     * @return
     */
    double printOwingRefactor() {
        if (basePrice() > 1000) {
            return basePrice() * 0.95;
        } else {
            return basePrice() * 0.98;
        }
    }
    double basePrice() {
        return quality * itemPrice;
    }
}

分解条件表达式(Decompose Conditional)

复杂条件逻辑容易导致复杂度上去,大型函数会使代码可读性降低。可从if,then,else三个中分别提炼出独立函数。
代码示例:

public class DecomposeConditional {
    /**
     * 重构前
     */
    void refactorBefore() {
        if (data.before(SUMMER_START) || data.after(SUMMER_END)) {
            charge = quantity * winterRate + winterServiceCharge;
        } else {
            charge = quantity * summerRate;
        }
    }
    /**
     * 重构后
     */
    void refactorAfter() {
        if (notSummer(data)) {
            charge = winterCharge(quantity);
        } else {
            charge = summerCharge(quantity);
        }
    }
    ....
}

提炼类(Extract class)

某类做了两个类应该做的事,建立一个新类,将相关的字段和函数从旧类搬移至新类。
具体做法:

1. 新建类,从新类中分离旧类责任,建立“从旧类访问新类”(尽量单向,否则依赖接口)连接关系;
2. 逐步移动字段(Move Field);
3. Move Method;精简类接口,判断是否需要公开此类。

代码示例:

/**
 * 重构前
 */
class Person {
    private String name;
    private String officeAreaCode;
    private String officeNumber;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getOfficeAreaCode() {
        return officeAreaCode;
    }
    public void setOfficeAreaCode(String officeAreaCode) {
        this.officeAreaCode = officeAreaCode;
    }
    public String getTelephoneNumber() {
        return "(" + officeAreaCode + ")" + officeNumber;
    }
    public void setOfficeNumber(String officeNumber) {
        this.officeNumber = officeNumber;
    }
}
/**
 * 重构后
 */
class PersonRefactor {
    //2.建立从Person到TelePhoneNumber的连接
    TelePhoneNumber telePhoneNumber = new TelePhoneNumber();
    
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    TelePhoneNumber getOfficeTelephone() {
        return telePhoneNumber;
    }
    
    public String getTelephoneNumber() {
        return telePhoneNumber.getTelephoneNumber();
    }
}
//1.将电话号相关行为分离至独立类中
class TelePhoneNumber {
    //3.Move Field移动一个字段
    private String areaCode;
    private String number;
    //4.Move Method将相关函数移动至TelePhoneNumber类中
    public String getAreaCode() {
        return areaCode;
    }

    public void setAreaCode(String areaCode) {
        this.areaCode = areaCode;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public String getNumber() {
        return number;
    }

    public String getTelephoneNumber() {
        return "(" + areaCode + ")" + number;
    }
}

提炼接口(Extract Interface)

若干用户使用类接口中的同一子集,或者两个类的接口中有部分相同,那么将相同的子集提炼至一个独立接口中。
具体做法:

1. Extract interface只能提炼共通接口,不能提炼共通代码。新建空接口;
2. 在接口中声明待提炼类的共通操作;让相关类实现上述接口;
3. 调整客户类型声明,令其使用该接口。

补充:多态满足的三个条件:继承;重写;父类引用指向子类对象:Parent p = new Child(); 实现多态的三种方法:重写、接口、抽象类和抽象方法。
代码示例:

public class ExtractInterface {
    /**
     * 重构前
     */
    double charge(Employee emp, int days) {
        int base = emp.getRate() * days;
        if (emp.hasSpecialSkill()) {
            return base * 1.05;
        } else {
            return base;
        }
    }

    /**
     * 重构后;3.调整客户端声明,令其使用该接口
     */
    double charge(Billable emp, int days) {
        int base = emp.getRate() * days;
        if (emp.hasSpecialSkill()) {
            return base * 1.05;
        } else {
            return base;
        }
    }
}

/**
 * 1.新建空接口,在接口声明待提炼类的共通操作
 */
interface Billable {
    //员工级别
    public int getRate();
    //是否有特殊技能
    public boolean hasSpecialSkill();
}

/**
 * 2.让Employee实现上述接口
 */
class Employee implements Billable {
   //....
}

以函数取代参数(Replace Parameter with Methods)

将参数计算过程提炼至独立函数中;本体内引用该函数的地方改为调用新建的函数;替换完后,修改并测试;全部替换后,移除参数。
代码示例:

public class ReplaceParameterWithMethods {
    /**
     * 重构前
     */
    void beforeRefactor() {
        //...
        int basePrice = _quantity * _itemPrice;
        discountLevel = getDiscountLevel();
        double finalPrice = discountedPrice(basePrice, discountLevel);
    }

    /**
     * 重构后
     */
    void beforeRefactor() {
        //...
        int basePrice = _quantity * _itemPrice;
        double finalPrice = discountedPrice(basePrice);
    }

    private double discountedPrice(int basePrice) {
        discountLevel = getDiscountLevel();
    }
}

保持对象完整(Preserve Whole Object)

假设你从某对象取出若干值,将它们作为某一次调用时的参数。不如改为传递整个对象。
代码示例:

public class PreserveWholeObject {
    /**
     * 重构前
     */
    int low = daysTempRange().getLow();
    int high =  daysTempRange().getHigh();
    withinPlan = plan.withinRange(low, high);

    /**
     * 重构后
     */
    withinPlan = plan.withinRange(daysTempRange());
}

隐藏“委托关系”(Hide Delegate)

客户通过一个委托类来调用另一个对象,在服务类上建立客户所需要的所有函数,用以隐藏委托关系。
代码示例:

public class HideDelegate {
    /**
     * 重构前
     */
    //此时调用链为 manager=john.getDepartment().getManager();
    class Person {
        Department department;

        public Department getDepartment() {
            return department;
        }

        public void setDepartment(Department department) {
            this.department = department;
        }
    }
    
    class Department {
        private String chargeCode;
        private Person manager;
        
        public Department(Person manager) {
            this.manager = manager;
        }

        public Person getManager() {
            return manager;
        }
        //...
    }

    /**
     * 重构后
     */
    //2.调整用户,令它只调用服务对象提供的函数
    // manager=john.getManager();
    class PersonRefactor {
        Department department;

        public Department getDepartment() {
            return department;
        }

        public void setDepartment(Department department) {
            this.department = department;
        }
        //1.对于每一个委托关系中的函数,在服务对象端建立一个简单的委托函数
        public Person getManager() {
            return department.getManager();
        }
    }
}

内联函数(Inline Method)

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

/**
 * 重构前
 */
int getRating() {
    return moreThanFiveLateDeliveries() ? 2 : 1;
}

private boolean moreThanFiveLateDeliveries() {
    return numberOfLateDeliveries > 5;
}
/**
 * 重构后
 */
int getRatingRefactor() {
    return (numberOfLateDeliveries > 5) ? 2 : 1;
}

引入外加函数(Introduce Foreign Method)

你需要为提供的类增加一个函数,但你无法修改这个类。在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。
代码示例:

/**
 * 代码重构前
 */
public void beforeRefactor() {
    Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
}

/**
 * 代码重构后
 */
public void afterRefactor() {
    Date newStart = nextDay(previousEnd);
}

private static Date nextDay(Date previousEnd) {
    return new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
}

引入本地扩展(Introduce Locale Extension)

你需要为服务类额外提供一些函数,但你无法修改这个类:建立新类,使它包含这些额外函数。让这个扩展品成为源类的子类或包装类。
代码示例:

//1.建立扩展类,将它作为原始类的子类
class MfDataSub extends Date {
    public MfDataSub(String dataString) {
        super(dataString);
    }
    //2.在扩展类中加入转型构造函数
    public MfDataSub(Date arg) {
        super(arg.getTime());
    }

    //3.添加新特性,Move Method将所有外加函数搬移至扩展类
    Date nextDay() {
        return new Date(getYear(), getMonth(), getDate() + 1);
    }
}

以委托取代继承(Replace Inheritance with Deegation)

某子类只使用超类接口中的一部分,或是根本不需要继承而来的数据,在子类中新建一个字段用于保存超类,调整子类函数,令它改成委托超类,去掉两者间继承关系。
代码示例:

/**
 * 重构前
 */
class MyStack extends Vector {
    public void push (Object element) {
        insertElementAt(element, 0);
    }
    
    public Object pop() {
        Object result = firstElement();
        removeElementAt(0);
        return result;
    }
}
/**
 * 重构后
 */
//3.去除两个类之间的继承关系,新建受拖累对象赋给受托字段
class MyStackRefactor {
    //1.在子类中新建一字段,引用超类实例,将其初始化为this
    private Vector vector = new Vector();
    //2.修改子类中的所有函数,让其不再使用超类,转而使用上述的委托字段
    public void push (Object element) {
        vector.insertElementAt(element, 0);
    }

    public Object pop() {
        Object result = vector.firstElement();
        vector.removeElementAt(0);
        return result;
    }
    //4.针对客户端所用的每一个超类函数,为它添加一个简单的委托函数。
    public int size() {
        return vector.size();
    }
    
    public boolean isEmpty() {
        return vector.isEmpty();
    }
}

代码中的“坏味道”

本节主要讲什么是“坏味道”。嗅到代码中的“坏味道”是在培养自己的判断力,即判断一个类中有多少实例变量算是太大,一个函数有多少行代码才算太长等。本节中的小标题格式为:代码中的坏味道+解决方案。

重复代码:提炼函数;以查询取代临时元素。

假设你在一个以上的地点看到相同的程序结构,设法将它们合二为一。步骤如下:

 1. 最简单的重复代码是利用Extract method提炼重复代码;
 2. 互为兄弟的子类含相同表达式,两个类先使用Pull up method将其推入超类中,若相似而非相同,应当使用Extract method将相似/差异部分隔开。
 3. 如果两个无关的类出现重复代码,那么使用Extract class将重复代码提炼到一个独立类中,在另一个类中调用新类,且需判断放在哪里最合适。

过长函数:分解条件表达式

程序越长越难以理解。小函数易理解的关键是有好名字,他人仅通过名字便可了解函数作用。步骤如下:

1.代码需要注释说明点什么,抽出独立函数并以其用途而非实现手法命名;
2.条件表达式和循环也是提炼信号,条件表达式可使用Decompose Conditional处理表达式。
3.运用Extract Method将许多参数和临时变量当做参数传递给被提炼出来的函数;
4.运用Replace Temp With Query来消除临时元素。

过长的类:提炼类、提炼接口

过长的类会导致代码重复、混乱。Extracrt class将几个变量提炼至新类,选择彼此相关的变量;若提炼适合作为一个子类,Extract subClass往往简单。有个小技巧:使用Extract Interface为每一种使用方式提炼出一个接口,可以帮你分解此类。

过长参数列表:以函数取代参数;传值改为传对象;组装对象

太长参数会导致前后不一致、难以理解、不易使用;全局对象有很多弊端;对象可以有效地解决这一切。

1. 如果向已有对象发出一条请求可以取代一个参数,那么应该使用Replace Parameter with Methods;
2. 可以使用Preserve Whole Object来将同一对象一堆数据收集起来;
3. 某些数据缺乏合理归属,使用Introduce Parameter Object(引入参数对象)为他们制造一个参数对象。

发散式变化:提炼类、细粒度化

某个类因不同的原因在不同的方向上发生变化,针对外界变化的所有相应修改,都只应发生在单一类中,找出某特定原因造成的所有变化。使用Extract Class将它们提炼至另一个类(有可能是子类)中。(拆类、细粒度化)

霰弹式修改:收敛类

每遇到变化,你得在许多不同的类中做出许多小修改,不累么?步骤如下:

使用Move Method(当两类之间太多耦合,将某个类中的方法移动至另一类中)和Move Field(在目标类中新建一个字段,修改源字段的所有用户,令它们改用新字段)将需要修改的代码放在同一类中,若没有,就创造一个,将一系列行为放进同一类中。(收敛类,将一系列相关行为放入同一类中)

依恋情结:消除分散,加强封装

函数对别的类的调用超过对自己所处类的调用。Extract Method 和Move Method用起来。(数据+操作=封装)
假设某函数会用到几个类的功能。判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据放在一起。保持变化只在一处发生。(消除分散+加强封装)

数据泥团:抽出新对象

很多地方都会看到相同的三四项数据:两个类中的相同字段、函数签名中的相同参数等等。解决方案如下:

1. 针对相同字段,运用Extract Class将它们提炼至独立对象;
2. 针对签名中的相同参数,运用Introduce ParameterObject(引入参数对象:以一个对象取代这些参数,因某些参数总是很自然的出现)或Preserve Whole Object(保持对象完整)。
3. 不必在意只用到新对象的一部分,只要新对象取代两个/更多的字段,就可行。

基本类型偏执:以对象取代基本类型

数据有两种:结构型(String,Data)和基本类型(int,double)。尽量以对象取代基本类型:

1.使用Replace Data Value with Object(以对象取代数据值)将本单独存在的数据值替换为对象。
2.使用Replace Array With Object(对象取代数组,对于数据组中的每个元素,以一个字段来表表示)。
3.总之:灵活使用、抽出总在一起的数据并封装对象。

switch过多:改为多态

面向对象即少用switch,switch意味着重复,使用多态解决。

1. Extract Method将switch提炼到一个独立函数中,
2. Move Method将其搬到需要多态性的类中。此时,你可使用Replace Type Code With SubClass(以子类取代类型码:针对一个不可变的类型码,它会影响类的行为)或Replace Type Code With state/strategy(以状态对象取代类型码:针对类型码,它会影响类的行为,且无法通过继承来消除)
3. 如果只在单一函数中有些选择示例,且不想改动它们,那么Replace Parameter with Explicit Methods(以明确函数取代参数:针对参数的每一个可能值,建立独立函数。针对函数取决于参数值而选择不同行为)是个不错的选择。

平行继承体系:消除重复、消灭类似父类

当你为某类新加一个子类,那么必须要在另一个类中也要新加子类。消除重复性的步骤是:

让一个继承体系的实例引用另一个继承体系的实例,在使用Move Method和Move Field将引用端继承体系消灭掉。

冗赘类:折叠继承体系、内联化类

1. 针对子类未做足够工作的情况,使用Collapse Hierarchy(折叠继承体系:超类和子类之间并无太大区别,将它们合为一体)来做足够工作;
2. 针对几乎没用的组件,可以使用Inline class(将类内联化:某个类没做太多事情,将这个类的所有特性搬移至另一个类中,然后移除原类)来处理。

夸夸其谈未来性:警惕代码过度/提前设计

过度设计会导致系统更难理解和维护。

1. 假设某抽象类没啥用,使用Collapse Hierarchy(折叠继承体系);
2. 假设函数无必要,使用Inline class(将类内联化);
3. 假设参数未用上,使用Remove Parameter(移出参数:函数本体无需某参数,将该参数去掉);
4. 假设函数命名有点扯,使用Rename Method(函数改名:函数名称未能揭示函数的用途,修改函数名称)

令人迷惑的临时变量:提炼类、减少临时变量使用

看到一个未使用过的临时变量,会让人疯的。

假设类中有复杂算法,牵扯到好几个变量,实现者不希望传一堆参数,那么Extract Class将这些变量和相关函数提炼到一个独立类中,提炼后的新对象将是一个函数对象。

过度耦合消息链:隐藏“委托关系”、减少链式调用

一对象请求另一对象,再往后请求另一个…。消息链的产生让代码结构耦合,这时应当使用Hide Delegate(隐藏“委托关系”)。看看消息链最终得到的对象是干什么,Extract Method提炼独立函数,Move Method推入消息链。

中间人:移除中间人、内联函数

对象的封装特点是对外隐藏细节。封装伴随着委托,也要防止过度委托。

1. 某类接口超过半数函数都委托给其他类是不合适的,使用Remove Middle Man(移除中间人:某个类做了过多的简单委托动作,让客户直接调用委托类,与Hide Delegate恰好相反)2. 函数“不干实事”,可以使用Inline Method(内联函数:一个函数的本体与名称同样清晰易懂,在函数调用点插入函数本体、然后移除该函数)

狎昵关系:改双向关联为单向关联,提炼类

恋人之间花费较多时间探究彼此private的东西无可厚非,但过分狎昵的类必须拆散。

1. 使用Move Method和Move Field划清界限;
2. 尝试运用Change Bidirectional Association to Undirectional(将双向关联改为单向关联:两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。因此去除不必要的关联);
3. 如果两个类实在情投意合,那么Extract Class将共同点提炼至安全地点,
4. 或者使用HideDeletegate隐藏“委托关系”。甚至可以去掉子类与超类的继承关系。

异曲同工的类:重命名、搬移函数、提炼超类

1. 如果两个函数做同一件事却拥有不同的名称,使用Rename Method(函数重命名)2. 如果不够,可以使用Move Method(搬移函数:将某些行为移入类,直到两者协议一致)
3. 如果代码仍然有冗余,那么Extract SuperClass(提炼超类:两个类有相似特性,为两个类建立超类,将相同特性移至超类)

不完美的库类:引入外加函数、引入本地扩展

类库不够完美,在库上做一层封装。使用Introduce Foreign Method(引入外加函数)或Introduce Locale Extension(引入本地扩展)。

纯稚数据类:封装字段;移除设值函数;隐藏函数。

1. 数据类只有字段和访问字段的函数,使用Encapsulate Field(封装字段:你的类中存在一个public字段,将它声明为private,并提供相应的访问函数);
2. 如果有集合,那么使用Encapsulate Collection(封装集合:有的返回函数有集合,让这个函数返回该集合的一个只读副本;并提供添加/移除集合元素的函数);
3. 如果类中有某些字段不想被修改,那么使用Remove Setting Method(移除设置函数:类中的某个字段应该在对象创建时被设值,然后就不再改变,去掉该字段的设值函数,将该字段声明为final)4. 如果函数没啥用,可以使用Hide Method(隐藏函数:当一函数未被任何类调用或者提供过多行为的接口时,就必须将该函数声明为private

被拒绝的馈赠:字段/方法下移;以委托取代继承

1. 所有超类都应该是抽象的。子类应当继承自超类的函数和数据,
2. 但如果子类不想继承,新建子类的兄弟类,使用Push Down Method(方法下移:超类中的某函数只与部分子类相关,将这个函数移到相关子类中去)和Push Down Field(字段下移:超类中的某字段只与部分子类相关,将这个字段移到相关子类中去)。
3. 如果不想修改继承体系,那么使用Replace Inheritance with Delegation(以委托取代继承)来达到目的。

过多的注释:记录将要干什么、无十足把握代码

当你感觉需要撰写注释时,可以尝试重构,试着让注释变得多余。如果你需要记录将要干什么或者对这部分代码无十足把握(自己“为什么做某事”),这时需要写注释。