您的位置:首页 > 编程语言

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

2017-10-27 15:20 197 查看

概述

1.1 参考资料

《重构-改善既有代码的设计》读后总结

《重构改善既有代码的设计》

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 总结

文中只是把常用到的,比较好表述的重构方法或情况总结了一下,并没有覆盖到书中的所有情况,如果对重构非常有兴趣的话建议大家阅读原书。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  重构