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