文档章节

代码重构之重新组织函数

ChinaHYF
 ChinaHYF
发布于 09/16 13:52
字数 4111
阅读 42
收藏 1

近期温习了重构经典著作《重构-改善既有代码的设计》,还是要把看书的一些理解和问题记录和分享出来。并且是一个持续跟进优化的过程。陆续把书中讲述的重构方法列举一下。

重构的很大一部分工作就是拆解过长函数(Long Method)。重新组织函数部分,书中介绍的有以下常见方法:

  • 提炼方法(Extract Method)

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

    1. 动机:

当方法过长或者一个方法中可以分开小的部分,每个部分都需要一段注释介绍来让人理解代码的用途就需要考虑把方法进行拆解,拆分的粒度的话可以按照每段注释来拆解方法。这样做的好处:1. 如果方法粒度比较小,方法被复用的机会就更大;2. 让高层函数想一些列注释;3. 方法的覆写(override)变得更容易;4. 如果有单元测试的话,测试用例也更方便覆盖。

    2. 做法以及注意点:

首先,每创造一个新方法,务必要起一个合适的名字(以它“做什么”来命名,而不是“怎样做”来命名)。关于如何恰如其分的为方法命名,后续也专门整理提供一片博文。

    3. 代表性示例:

一般遇到需要使用 提炼函数(Extract Method)方法的场景有:“无局部变量”;“有局部变量”;“对局部变量再赋值” 三种情况,相比之下而“对局部变量再赋值”最为复杂,所以我这里直接给出“对局部变量再赋值”的示例

1. 第一种情况:

void printOwing() {
    Enumeration enumeration = _orders.elements();
    double outstanding = 0.0;

    printBaner();

    //calculate outstanding
    while (enumeration.hasMoreElements()) {
        Order each = (Order) enumeration.nextElement();
        outstanding += each.getAmount();
    }

    printDetails(outstanding);
}

把“calculate outstanding”部分代码提炼出来

void printOwing() {
    printBanner();
    double oustanding = getOutstanding();
    printDetails(oustanding);
}

double getOutstanding() {
    Enumeration enumeration = _orders.elements();
    double outstanding = 0.0;

    while (enumeration.hasMoreElements()) {
        Order each = (Order) enumeration.nextElement();
        outstanding += each.getAmount();
    }
    return outstanding;
}
注:enumeration 只在被提炼代码段中使用,所以可以将它整个搬到新方法中。
但是 outstanding 在被提炼代码的内外都使用到了,所以必须让提炼出来的方法返回。编译通过之后,把回传值改名如下:
double getOutstanding() {
    Enumeration enumeration = _orders.elements();
    double result = 0.0;

    while (enumeration.hasMoreElements()) {
        Order each = (Order) enumeration.nextElement();
        result += each.getAmount();
    }
    return result;
}

2. 第二种情况:

在1的示例中,outstanding变量只是单纯被初始化为一个明确的值。没有做任何处理,所以就可以放在提炼的方法中进行初始化,如果除了初始化之后,还需要做其它处理,那么就需要将其作为一个入参,传到提炼出来的方法中,如下:

void printOwing(double previousAmount) {
    Enumeration enumeration = _orders.elements();
    double outstanding = previousAmount * 1.2;

    printBaner();

    //calculate outstanding
    while (enumeration.hasMoreElements()) {
        Order each = (Order) enumeration.nextElement();
        outstanding += each.getAmount();
    }

    printDetails(outstanding);
}

提炼函数(Extract Method)后的结果:

void printOwing(double previousAmount) {
    double outstanding = previousAmount * 1.2;
    printBanner();
    outstanding = getOutstanding();
    printDetails(outstanding);
}

double getOutstanding(double initialValue) {
    double result = initialValue;
    Enumeration enumeration = _orders.elements();

    while (enumeration.hasMoreElements()) {
        Order each = (Order) enumeration.nextElement();
        result += each.getAmount();
    }
    return result;
}
这样,编译测试通过之后,还可以把 outstanding 的初始化过程整理一下:
void printOwing(double previousAmount) {
    printBanner();
    outstanding = getOutstanding(previousAmount * 1.2);
    printDetails(outstanding);
}
  •  内联函数(Inline Method)

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

    1. 动机: 

当发现一个方法内有很多不合理的方法,这个时候,就可以把这些方法都内联回调用方法的这个大方法中,重新再对方法进行提炼抽取的时候使用。
    2. 代表性示例:

int getRating() {
    return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() {
    return _numberOfLateDeliveries > 5;
}

重构为

int getRating() {
    return (_numberOfLateDeliveries > 5) ? 2 : 1;
}
  •  内联临时变量(Inline Temp)

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

    1. 动机: 

当你发现某个临时变量被赋予某个函数调用的返回值。这样的临时变量不会有任何危害,但是如果这个临时变量妨碍了其它的重构方法,这个时候,就需要将其内联化。
    2. 做法以及注意点:

首先确保临时变量只是被赋值了一次,然后其它地方都仅仅是在引用而没有再对其赋值,这个时候可以将其内联

首先如果要内联的变量没有被声明为 final 类型,就先把他声明为 final 类型,然后编译。这样可以检查这个临时变量是否真的只被赋值一次。如果确实只被赋值一次,那就把所有的引用点,替换为“为临时变量赋值”的表达式。

  • 已查询替代临时变量(Replace Temp with Query)

程序中以一个临时变量保存某个表达式的运算结果,将这个表达式提炼到一个独立方法中,将这个临时变量的所有引用点替换为对新方法的调用。此后,新方法可以被其它方法调用。

    1. 动机: 

临时变量的问题在于:他们是暂时的,并且只在所属的方法内可见,而且只能在所属方法中调用,所以一个类中的多个方法都需要用到某些临时变量的时候,如果把临时变量替换为一个查询,那么同一个类中的所有方法都可以获取这份信息。

局部变量回事代码难以被提炼,所以应该尽可能把它们替换为查询式。
    2. 做法以及注意点:

一般情况下临时变量只被赋值一次,但是如果临时变量是用来收集结果的(比如循环中的累加值),就需要将某些程序逻辑(比如循环)也复制到查询方法中去。
    3. 代表性示例:

double getPrice() {
    int basePrice = _quantity * _itemPrice;
    double discountFactor;
    if(basePrice > 1000) {
        discountFactor = 0.95;
    } else {
        discountFactor = 0.98;
    }
    return basePrice * discountFactor;
}

我们希望把两个临时变量都替换掉,结果如下:

//首先,先把它们设置成final类型,进行编译,确保它们只是被简单赋值一次
double getPrice() {
    final int basePrice = _quantity * _itemPrice;
    final double discountFactor;
    if(basePrice > 1000) {
        discountFactor = 0.95;
    } else {
        discountFactor = 0.98;
    }
    return basePrice * discountFactor;
}
//上面操作完之后,编译没有问题的话,接下来提炼赋值动作右侧的表达式
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;
}

 接下来,使用 Inline Temp 来进一步重构

//basePrice的使用一共有两处,首先替换第一处,然后进行编译测试。
double getPrice() {
    final int basePrice = getBasePrice();
    final double discountFactor;
    if(getBasePrice() > 1000) {
        discountFactor = 0.95;
    } else {
        discountFactor = 0.98;
    }
    return basePrice * discountFactor;
}

private int getBasePrice() {
    return _quantity * _itemPrice;
}
//发现没有问题之后,进行第二处替换,并且可以直接把 basePrice 的声明就去掉了。
double getPrice() {
    final double discountFactor;
    if(getBasePrice() > 1000) {
        discountFactor = 0.95;
    } else {
        discountFactor = 0.98;
    }
    return getBasePrice() * discountFactor;
}

private int getBasePrice() {
    return _quantity * _itemPrice;
}
//然后,通过类似方法重构 discountFactor
double getPrice() {
    return getBasePrice() * getDiscountFactor();
}

private int getDiscountFactor() {
    if(getBasePrice() > 1000) {
        return 0.95;
    } else {
        return 0.98;
    }
}

private int getBasePrice() {
    return _quantity * _itemPrice;
}

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

当方法中存在复杂的表达式的时候,将该表达式(或其中的一部分)的结果放进一个临时变量,以变量的名称来解释表达式的用途   

    1. 动机: 

复杂的表达式一般难以阅读,这种情况下,最好使用含义明确的临时变量来替换。适合使用Introduce Explaining Variable的场景:在条件逻辑中,可以一个良好命名的临时变量来解释对应条件子句的意义;在较长的算法中,应用临时变量来解释每一步运算的含义。
    2. 做法以及注意点:

依然声明 final 修饰的临时变量,这样可以保证临时变量的单一职责。然后进行替换。
    3. 代表性示例:

double price() {
    //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);
}

//先把底价=数量*单价提取到临时变量中,并且有两处使用的地方,逐一替换
double price() {
    final double basePrice = _quantity * _itemPrice;
    //price is base price - quantity discount + shipping
    return basePrice -
            Math.max(0, _quantity - 500) * _itemPrice * 0.05 +
            Math.min(basePrice * 0.1, 100.0);
}

//将 quantity discount 再提取出来
double price() {
    final double basePrice = _quantity * _itemPrice;
    final double quantityDiscount = Math.max(0, _quantity - 500) * _itemPrice * 0.05;
    //price is base price - quantity discount + shipping
    return basePrice -
            quantityDiscount +
            Math.min(basePrice * 0.1, 100.0);
}

//最后再把运费 shipping 提取出来
double price() {
    final double basePrice = _quantity * _itemPrice;
    final double quantityDiscount = Math.max(0, _quantity - 500) * _itemPrice * 0.05;
    final double shipping = Math.min(basePrice * 0.1, 100.0);
    //price is base price - quantity discount + shipping
    return basePrice -
            quantityDiscount +
            shipping;
}

然后,通常不以变量来解释动作含义,我们再通过 Extend Method 进一步重构

double price() {
    //price is base price - quantity discount + shipping
    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(basePrice * 0.1, 100.0);
}

这里就自然出现一个问题:那么像上面这种情况下,重构应该止于什么地方呢,作者在书中表述:在进行 Extend Method 需要花费更大的工作量的时候。如果要处理的方法是一个拥有大量局部变量的方法,那么要使用 Extend Method 短期内,变得比较困难,这个时候就到 Introduce Explaining Variable 为止是比较合适的。这里我比较认同作者的观点。

  • 分解临时变量(Split Temporary Variable)

当程序中出现临时变量被赋值超过一次,它既不是循环变量,又不是用于收集计算结果。那么就应该针对每次赋值,创造一个独立,对应的临时变量。    

    1. 动机: 

如果一个临时变量被赋值超过一次,就意味着它在方法中承担了一个以上的责任。如果临时变量承担多个责任,就应该分解为多个临时变量,保证每个临时变量只承担一个责任。
    2. 做法以及注意点:

通常将新的变量声明为 final。在临时变量第二次赋值动作为界。修改此前对该变量的所有引用,让它们引用新的临时变量。
    3. 代表性示例:

//这里是计算一个运动举例。在起点处,静止的物体收到一个初始力的作用而开始运动。一段时间后,第二个力作用于这个五次,让他再次加速。根据牛顿第二定律,计算物体运动的举例
double getDistanceTravelled(int time) {
    double result;
    double acc = _primaryForce / _mass;
    int primaryTime = Math.min(time, _delay);
    result = 0.5 * acc * 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;
}

重构之后,

double getDistanceTravelled(int time) {
    double result;
    final double primaryAcc = _primaryForce / _mass; 
    int primaryTime = Math.min(time, _delay);
    result = 0.5 * primaryAcc * 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;
}

 接下来,还可以继续应用其它重构方法,让方法变得更加易读。

  • 移除对参数的赋值(Remove Assignment to Parameters)

对一个参数进行赋值的时候,以一个临时变量取代改参数的位置

    1. 动机: 

如果直接对方法的入参,进行赋值操作。会降低代码的清晰度,容易让人造成理解偏差而产生误会。
    2. 做法以及注意点:

建一个临时变量,把待处理的参数赋值给这个临时变量。
    3. 代表性示例:

int discount(int inputVal, int quatity, int yearToDate) {
    if(inputVal > 50) {
        inputVal -= 2;
    }
    if(quatity > 100) {
        inputVal -= 1;
    }
    if(yearToDate > 10000) {
        inputVal -= 4;
    }
    return inputVal;
}
/************************************************************/
//重构之后
int discount(int inputVal, int quatity, int yearToDate) {
    int result = inputVal;
    if(inputVal > 50) {
        result -= 2;
    }
    if(quatity > 100) {
        result -= 1;
    }
    if(yearToDate > 10000) {
        result -= 4;
    }
    return result;
}

这里其实涉及到一个java方法的“按值传递 ”的细节

看下面的例子:

/**
 * Created by haoyufei on 18/9/16.
 */
public class Test {
    public static void main(String[] args) {
        int x = 5;
        triple(x);
        System.out.println("x after triple:" + x);
        
    }
    private static void triple(int arg) {
        arg = arg * 2;
        System.out.println("x in triple:" + arg);
    }

}

结果如下:

第二个例子:

/**
 * Created by haoyufei on 18/9/16.
 */
public class Test {
    public static void main(String[] args) {
        Date date1 = new Date("16 Sep 18");
        nextDateUpdate(date1);
        System.out.println("date1 after nextDate:" + date1);

        Date date2 = new Date("16 Sep 18");
        nextDateReplace(date2);
        System.out.println("date2 after nextDate:" + date2);
    }

    private static void nextDateUpdate(Date arg) {
        arg.setDate(arg.getDate() + 1);
        System.out.println("date1 in nextDate:" + arg);
    }

    private static void nextDateReplace(Date arg) {
        arg = new Date(arg.getYear(), arg.getMonth(), arg.getDay() + 1);
        System.out.println("date1 in nextDate:" + arg);
    }
}

结果如下:

这两个例子一个是传原生类型,一个是传对象。通过上面的两个例子,可以得出一个结论:在 java 中,对象的引用是按值传递的。所以,可以修改参数对象的内部状态,但对参数对象重新赋值是没有意义的。

  • 以方法对象取代方法(Replace Method with Method Object)

有一个大型方法,其中对局部变量的使用导致无法进行 Extend Method 的时候,将这个方法放入一个单独的对象中,这样局部变量就变成了对象内的字段。然后可以在同一个对象中将这个大型的方法分解为多个小方法。

    1. 动机: 

局部变量会增加方法分解的难度。如果一个方法中局部变量很多,但是又需要重构这个方法,这个时候就应该想到 Replace Method with Method Object 来把局部变量都变成方法对象的字段。
    2. 做法以及注意点:

创建一个新类,在新类中创建 final 字段,来把欧尼原先大型方法所在的对象。同时针对原方法中的每个临时变量和每个参数,在新类中建立一个对应的字段来保存。然后将原方法的代码放到新类中,并且创建一个compute()方法来存放。
    3. 代表性示例:

原先的类

class Account {
    int gamma(int inputVal, int quantity, int yearToDate) {
        int importantValue1 = (inputVal * quantity) + delta();
        int importantValue2 = (inputVal * yearToDate) + 100;
        if((yearToDate - importantValue1) > 100) {
            importantValue2 -= 20;
        }
        int importantValue3 = importantValue2 * 7;
        
        return importantValue3 - 2 * importantValue1;
    }
}

重构之后的样子

声明一个新类,提供一个final字段用来保存源对象
class Gamma{
    private final Account _account;
    private int inputVal;
    private int quantity;
    private int yearToDate;
    private int importantValue1;
    private int importantValue2;
    private int importantValue3;

    Gamma(Account source, int inputValArg, int quantityArg, int yearToDateArg) {
    	_account = source;
    	inputVal = inputValArg;
    	quantity = quantityArg;
    	yearToDate = yearToDateArg;
    }

    int compute() {
    	importantValue1 = (inputVal * quantity) + _account.delta();
    	importantValue2 = (inputVal * yearToDate) + 100;

    	if((yearToDate - importantValue1) > 100) {
            importantValue2 -= 20;
        }

    	int importantValue3 = importantValue2 * 7;
        
        return importantValue3 - 2 * importantValue1;
    }
}

//修改旧方法,让它将它的工作委托于刚建立的这个方法对象
int gamma(int inputVal, int quantity, int yearToDate) {
    return new Gamma(this, inputVal, quantity, yearToDate).compute();
}

 再之后可以进行其它重构,比如讲compute()方法中的某些部分可以提取出来

int compute() {
    importantValue1 = (inputVal * quantity) + _account.delta();
    importantValue2 = (inputVal * yearToDate) + 100;

    importantThing();

    int importantValue3 = importantValue2 * 7;

    return importantValue3 - 2 * importantValue1;
}

void importantThing() {
    if((yearToDate - importantValue1) > 100) {
        importantValue2 -= 20;
    } 
}
  • 替换算法(Substitute Algorithm)

这种方法比较常见和简单,我就没有再举例子。

注:博文中的例子,很大一部分是直接借鉴的 经典著作《重构-改善既有代码的设计》中的例子。

我自己的理解:书中描述的这些方法仅仅是一些借鉴的经验和手段,也是一些常规做法,至于重构的时候,方法的粒度往往需要自己在实践的过程中根据实际情况来确定,并没有一个放之四海而皆准的通用法则,并且根据个人的视角不同结果也会不同,就像那句话“一千个人心中有一千个哈姆雷特”一样。

并且进行重构的时候,每一步的跨度大小也是因人而异,熟练的高手可能在看到某种需要重构的代码的时候,一下就能够跳到最后一步。刚开始进行重构的话,还是需要小步快跑,多编译运行,多测试,要保证质量。

需要做的事情:

  1. 整理方法命名规则以及常见词汇查找文档或者规范,结合阿里的规范

© 著作权归作者所有

共有 人打赏支持
ChinaHYF
粉丝 14
博文 64
码字总数 61425
作品 0
西安
高级程序员
【原创】《重构》读书笔记

第1章 重构,第一个案例 第2章 重构原则 第3章 代码的坏味道 第4章 构筑测试体系 第5章 重构列表 第6章 重新组织函数 第7章 在对象之间搬移特性 第8章 重新组织数据 第9章 简化条件表达式 第...

pandudu
2015/12/23
54
1
[读书]读《重构-改善既有代码的设计》

读《重构-改善既有代码的设计》 断断续续,加上过年,花了快2个月吧,把《重构-改善既有代码的设计》读完了,这里总结下。 发现此书背景 读的感觉 知识感触 发现此书背景 这本书是从同事的桌...

zemel
2016/03/07
6
0
Introduce Parameter Object (引入参数对象)

Summary: 某些参数总是很自然地同时出现。以一个对象取代这些参数。 动机: 你经常会看到特定的一组参数总是一起被传递。可能有好几个函数都使用这一组参数,这些函数可能隶属于同一个类,也...

忆瑶
2014/04/09
0
0
重构-改善既有代码设计

重构是在不改变软件可观察行为的前提下,对代码作出修改,以改进程序的内部结构。本质上说就是在代码写好后改进它的设计 重构往往意味着不了解软件行为下重构程序 2.在设计前期使用模式常常导...

zhchl2010
2015/12/24
107
0
关于烂代码的那些事(上)

1.写烂代码很容易 刚入程序员这行的时候经常听到一个观点:你要把精力放在ABCD(需求文档/功能设计/架构设计/理解原理)上,写代码只是把想法翻译成编程语言而已,是一个没什么技术含量的事情...

莫铭
2016/08/02
21
0

没有更多内容

加载失败,请刷新页面

加载更多

c++_CWinApp

CWinApp:MFC 中的主应用程序类封装用于 Windows 操作系统的应用程序的初始化、运行和终止. ON_COMMAND(ID_FILE_NEW, &CWinApp::OnFileNew)ON_COMMAND(ID_FILE_OPEN, &CWinApp::OnFileOpen...

一个小妞
6分钟前
0
0
可变对象传入set

set=([1,2,3]), 其中([ ])只是set的表现形式,并不是把list放入set中 >>> s1=set([1,2,3])>>> L[1, 2, 3]>>> s1{1, 2, 3}>>> s1(L)Traceback (most recent call last): File "<std......

fadsaa
9分钟前
0
0
多生产与多消费:操作栈

代码同“多生产与一消费”,区别在于测试类代码 public class Test { public static void main(String[] args) { MyStack myStack = new MyStack(); Produce produce......

起个昵称好难啊
10分钟前
0
0
@Component 的作用

1、@controller 控制器(注入服务) 用于标注控制层,相当于struts中的action层 2、@service 服务(注入dao) 用于标注服务层,主要用来进行业务的逻辑处理 3、@repository(实现dao访问) ...

踏破铁鞋无觅处
10分钟前
0
0
工厂模式

工厂方法模式 一个抽象产品类,可以派生出多个具体产品类。 一个抽象工厂类,可以派生出多个具体工厂类。 每个具体工厂类只能创建一个具体产品类的实例。 简单工厂模式 又称静态工厂方法模式...

noob_fly
13分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部