重构—改善既有代码的设计

概述

1.1 参考资料

  1. 《重构-改善既有代码的设计》读后总结
  2. 《重构改善既有代码的设计》
  3. 22种代码的坏味道,一句话概括

1.2 何谓重构

首先要说明的是:视上下文不同,重构的定义可以分为名词和动词两种形式。
1. 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
2. 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

根据重构的定义可知,重构其实就是优化代码的结构,使其阅读性更好。需要强调的是:重构不能改变软件可观察的行为——重构之后软件功能一如既往。任何用户,不论最终用户或其它程序员,都不知道已经有东西发生了变化。

1.3 为何重构

重构可以帮助你始终良好的控制自己的代码。重构就是一个工具,它可以用于以下几个目的。

1.3.1 重构改进软件设计

当人们只为短期目的,或是在完全理解整体设计之前,就贸然修改代码,程序将逐渐失去自己的结构,就很难通过阅读源码而理解原来的设计。重构很像是在整理代码,你所做的就是让所有东西回到应处的位置上。代码结构的流失通常是累积性的,因此经常性的重构可以帮助代码维持自己该有的形态。

1.3.2 重构使软件更容易理解

其实写程序,就是和计算机在交谈:你编写代码告诉计算机做什么事,它的响应则是精确按照你的指示行动。当然除了计算机外,你的源码还有其他阅读者:未来可能会有另一位程序员尝试读懂你的代码并做一些修改。我们写代码的时候很容易忘记这位未来的程序员,但他才是最重要的。如果一个程序员不理解你的代码,可能需要很长的时间来修改代码——而他理解了你的代码,这个修改或许只需要花费一个小时。
其实很多时候那个未来修改你程序的开发者就是你自己。此时重构就显得尤其重要。利用重构可以协助你理解不够熟悉的代码,而且重构会把你带到更高的层次上。

1.3.3 重构帮忙找到bug

对代码的理解,可以更好的找到bug。如果对代码进行重构,就可以深入理解代码的功能,搞清楚程序结构的同时,就更容易找出程序的bug。

1.3.4 重构提高编程速度

其实前面提到的一切都归结到最后一点:重构帮你更快速的开发程序。
听起来有点儿违反直觉。可以看出重构能够提高质量。如改善设计、提升可读性、减少错误,这些都是提高质量。但这难道不会降低开发速度吗?
良好的设计是快速开发的根本——事实上,拥有良好的设计才可能做到快速开发。如果没有良好的设计,或许某一段时间内你的进展迅速,但是不好的设计很快会让你的速度慢下来。时间最终都浪费在调试上面。因为你必须花更多的时间去理解系统、寻找重复代码。如此的循环下去。
良好的设计是维持软件开发速度的根本。重构可以帮助你更快速地开发软件,因为重构可以提高设计质量,避免重复的工作。

1.4 何时重构

三次法则 第一次只管做,第二次会产生反感,第三次就应该重构
添加功能时重构 当给软件添加新特性不方便时候,就应该重构
修补错误时重构 对代码进行重构,可以更方便的发现程序中的bug
复审代码时重构 重构可以帮助复审别人的代码

2 代码的坏味道

如果一段代码是不稳定或者有一些潜在问题,那么代码往往会包含一些明显的痕迹。正如食物要腐坏之前,经常会发出一些异味一样。Martin Fowler把有问题的代码称为“代码的坏味道”。接下来本文对22种代码的坏味道进行整理。

2.1 重复代码

Duplicated Code ——————— (重复代码) 难维护。
解决方法:提取公共函数。

2.2 过长函数

Long Method ————————–(过长函数) 难理解
解决方法:拆分成若干函数

2.3 过大的类

Large Class—————————-(过大的类) 难用、难理解
解决办法:拆分成若干类,过程中若遇到Duplicated Code,就提取公共函数。

2.4 过长参数列表

Long parameter List——————(参数多)调用难
解决方法:将参数封装成结构或者类

2.5 发散式变化

Divergent Change———————(发散式变化)发散式修改,改好多需求,都会动它。
解决方法:拆,将总是一起变化的东西放在一块儿。

2.6 霰弹式修改

Shotgun Surgery———————(霰弹式修改)散弹式修改,改某个需求时,都会动他。
解决方法:将各个修改点,集中起来,抽象成一个类。

2.7 依恋情结

Feature Envy———————(红杏出墙的函数)使用了大量其它类的成员。
解决方法:将这个函数挪到那个类里面。

2.8 数据泥团

Data Clumps———————(数据团)。
解决方法:他们那么有基情,就在一起吧,给他们一个新的类。

2.9 基本类型偏执

Primitive Obsession———————(偏爱基本类型) 热衷于使用int,double等基本类型。
解决方法:反复出现的一组参数,有关联的多个数组换成类。

2.10 Switch惊悚现身

Switcth Statements———————(switch 语句)。
解决方法:state/strategy或者时简单的多态。

2.11 平行继承体系

Parallel Inheritance Hierarchies———————(平行继承)增加A类的子类ax,B类也需要相应的增加一个bx。
解决方法:应该有一个类时可以去掉继承关系的。

2.12 冗赘类

Lazy Class———————(冗赘类) 如果他不干活了,炒掉他吧。
解决方法:把这些不再重要的类里面的逻辑,合并到相关类,删掉旧的。
Speculative Generality———————(夸夸其谈未来性)。
解决方法:删掉。

2.13 令人迷惑的暂时字段

Temporary Field———————(临时字段)发散式修改,改好多需求,都会动他。
解决方法:将这些临时变量集中到一个新类中管理。

2.14 过度耦合的消息链

Message Chains———————(消息链) 过度耦合才是坏的。
解决方法:拆函数或移动函数。

2.15 中间人

Middle Man———————(中间人) 大部分都交给中间人来处理了。
解决方法:用继承替代委托。

2.16 狎昵关系

Inappropriate Intimacy———————(太亲密) 相似的类,有不同接口。
解决方法:划清界限拆散,或合并,或改成单项联系。

2.17 异曲同工的类

Alternative Classes with Different Interfaces———————(相似的类,有不同接口)。
解决方法:重命名、移动函数、或抽象子类。

2.18 不完美的类库

Incomplete Lirary Class———————(不完美类库)。
解决方法: 包一层函数或包成新的类。

2.19 纯稚的数据类

Data Class———————(纯数据类)类很简单,仅有公共成员变量,或简单操作函数。
解决方法:将相关操作封装进去,较少public成员变量。

2.20 被拒绝的遗赠

Refused Bequest———————(继承过多) 父类里面方法很多,子类只用有限几个。
解决方法:用代理替代继承关系。

2.21 过多的注释

Comments———————(太多注释)这里指代码太难动了,不得不用注释解释。
解决方法:避免用注释解释代码,而是说明代码的目的,背景等。好代码会说话。

3 重构方法举例

3.1 重构函数

3.1.1 重复代码

这种情况应该很多人都遇到过,编程过程中要尽量避免重复的代码,解决方法是将重复的内容提炼到一个单独的函数中。

void A() {
    .....
    System.out.println("name" + _name);
}

void B() {
    .....
    System.out.println("name" + _name);
}

将代码更改为↓

void A() { .... }

void B() { .... }

void printName(String name) {
    System.out.println("name" + name);
}

3.1.2 内联临时变量

如果你对一个变量只使用了一次,那就不妨对它进行一次重构。

int basePrice = order.basePrice();
return (basePrice > 100);

更改为↓

 return (order.basePrice() > 1000);

3.1.3 尽量去掉临时变量

临时变量多了会难以维护,所以尽量去掉所使用的临时变量。

int area = _length * _width;
if (area > 1000) 
    return area * 5;
else
    return area *4;

更改为↓

if (area() > 1000) 
    return area() * 5;
else
    return area() *4;

int area() {
    return _length * _width;
}

3.1.4 引入解释性变量

跟上面那个相反,如果使用函数变得很复杂,可以考虑使用解释型变量了。

if ((platform.toUpperCase().indexOf("mac") > -1) &&
    (brower.toUpperCase().indexOf("ie") > -1) &&
    wasInitializes() && resize > 0) {
        ......
    }

更改为↓

final boolean isMacOS = platform.toUpperCase().indexOf("mac") > -1;
final boolean isIEBrowser = brower.toUpperCase().indexOf("ie") > -1;
final boolean wasResized = resize > 0;

if (isMacOS && isIEBrowser && wasInitializes() && wasResized) {
    ......
}

3.1.5 移除对参数的赋值

参数传入函数中,应该尽量避免对其进行更改。

int discount (int inputVal, int quantity, int yearToDate) {
    if (inputVal > 50) inputVal -= 2;
}

更改为↓

int discount (int inputVal, int quantity, int yearToDate) {
    int result = inputVal;
    if (result > 50) result -= 2;
}

另外,函数中声明的临时变量最好只被赋值一次,如果超过一次就考虑再声明变量对其进行分解了。
一个函数也不应该太长,如果太长首先影响理解,其次包含的步骤太多会影响函数复用。做法是将里面的步骤提取为很多小函数,并且函数命名要体现出函数做了什么,清晰明了。

3.2 重构类

3.2.1 搬移方法

每一个方法应该放在最适合的位置,不能随便乱放,所以很多时候你需要考虑,一个方法在这里是不是最适合的。

class Class1 {
    aMethod();
}

class Class2 {
}

更改为↓

class Class1 {
}

class Class2 {
    aMethod();
}

3.3 搬移字段

每一个字段,变量都应该放到其自己属于的类中,不能随便放,不属于这个类中的字段也需要移走。

class Class1 {
    aField;
}

class Class2 {
}

更改为↓

class Class1 {
}

class Class2 {
    aField;
}

3.4 提炼一个新类

将不属于这个类中的字段和方法提取到一个新的类中。所以说在你写代码的时候一定要考虑放这里是不是合适,有没有其他更合适的地方?

提炼到新的类中↓

3.5 简化条件表达式

3.5.1 分解条件表达式

有时候看着一个if else语句很复杂,我们就试着把它分解一下。

class Person {
    private String name;
    private String officeAreaCode;
    private String officeNumber;

    public String getTelephoneNumber() { ..... }
}

更改为↓

class TelephoneNumber {
    private String areaCode;
    private String number;

    public String getTelephoneNumber() { ..... }
}

class Person {
    private String name;
    private TelephoneNumber _officeNumber;
}

当然实际情况可能复杂的多,这样的重构才显得有意思,这里只是让大家脑子里有一个这样的思想,以后遇见这样的情况能想起来可以这样子重构。

3.5.2 分解条件表达式

有时我们写的多个if语句是可以合并到一起的。

if (isUp(case) || isLeft(case)) 
    num = a * b;
else num = a * c;

更改为↓

if (isTrue(case)) 
    numberB(a);
else numberC(a);

boolean isTrue(case) {
    return isUp(case) || isLeft(case);
}

int numberB(a) {
    return a + b;
}

int numberC(a) {
    return a + c;
}

3.5.3 合并重复的条件片段

有时候你可能会在if else 语句中写重复的语句,这时候你需要将重复的语句抽出来。

if (isSpecialDeal()) {
    total = price * 0.95;
    send();
} else {
    total = price * 0.98;
    send();
}

更改为↓

if (isSpecialDeal())
    total = price * 0.95;
else
    total = price * 0.98;

send();

3.5.4 以卫句取代嵌套表达式

这个可能有点难以理解,但是我感觉用处还是比较大的,就是加入return语句去掉else语句。

if (a > 0) result = a + b;
else {
    if (b > 0) result = a + c;
    else {
        result = a + d;
    }
}
return result;

更改为↓

if (a > 0) return a + b;
if (b > 0) return a + c;
return a + d;

是不是变得很简单,加入卫语句就是合理使用return关键字。有时候反转条件表达式也能简化if else语句。

3.5.5 以多态取代switch语句

这个我感觉很重要,用处非常多,以后你们写代码的时候只要碰到switch语句就可以考虑能不能使用面向对象的多态来替代这个switch语句呢?

int getArea() {
    switch (_shap)
        case circle:
            return 3.14 * _r * _r; break;
        case rect;
            return _width + _heigth;
}

更改为↓

class Shap {
    int getArea(){};
}

class Circle extends Shap {
    int getArea() {
        return 3.14 * _r * _r; break;
    }
}

class Rect extends Shap {
    int getArea() {
        return _width + _heigth;
    }
}

然后在调用的时候只需要调用Shap的getArea()方法就行了,就可以去掉switch语句了。然后我们还可以在一个方法中引入断言,这样可以保证函数调用的安全性,让代码更加健壮。

4 总结

文中只是把常用到的,比较好表述的重构方法或情况总结了一下,并没有覆盖到书中的所有情况,如果对重构非常有兴趣的话建议大家阅读原书。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值