田腾飞的博客

【编程素养】《重构-改善既有代码的设计》读后总结

最近刚刚毕业入职,刚来团队不是很忙,身边同事正好有一本经典书籍《重构-改善既有代码的设计》,这本书在我实习的时候团队周会上曾经大力推荐过。正好这次趁机借来阅读一遍😁。

强烈推荐大家阅读这本书,为什么呢?因为书皮上面写着“普通程序员进阶到编程高手必须修炼的秘籍”,这让我仿佛看到了我的“升值加薪,赢取白富美,走向人生巅峰”的道路🤒!

如果你没时间阅读也没关系,我就把我读后的总结写出来,把里面常用的重构手法总结一下,你看过以后一样可以说自己读过这本书,你的代码也会越来越优雅。哈哈,let’s go!

为啥要重构

先来解释一下何为重构:“重构就是对软件内部结构的一种调整,目的是不改变软件可观察行为的前提下,提高其可理解性,降低其可修改成本。”太专业的解释了。我下个通俗的定义吧:“重构是优化代码结构,使其阅读性更好,扩展性更强的一种高级技术”。

软件开发中,随着功能的加入,程序将慢慢失去原来的结构。有些人贸然加入功能的实现代码,但却没有理解原来程序的结构和考虑代码的扩展性,程序的可读性也非常低,随着代码数量越来越多,如果不进行重构的话,程序就会越来越难以维护,导致最后放弃这个程序。我们需要重构让程序避免这样的结果。

我觉得本书中有句话特别好”程序是首先写给人看的,其次才是写给机器看“,我们写程序不可能刚开始就会有这样很好的结构,在写的过程中我们需要对代码进行重构让其走上这样的道路,代码越来越能看的懂,将来接手维护这个程序的人也会上手更快。

多写测试

本书作者提倡大家多谢测试代码,每写完一个功能或做重构后就进行单元测试,总之多写测试代码,多测试自己的代码总是会有好处的。

书中列举了大量的不好的代码情况,这些不好的情况我们编程中都应该去避免,下面开始列举常见的需要重构的情况(并不包含本书的所有列举),以及重构手法。

重构函数

重复代码

这种情况应该很多人都遇到过,编程中不要有大量的重复代码,解决办法就是去提炼到一个单独的函数中。

1
2
3
4
5
6
7
8
9
void A() {
.....
System.out.println("name" + _name);
}
void B() {
.....
System.out.println("name" + _name);
}

更改为↓

1
2
3
4
5
6
7
void A() { .... }
void B() { .... }
void printName(String name) {
System.out.println("name" + name);
}

内联临时变量

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

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

更改为↓

1
return (order.basePrice() > 1000);

尽量去掉临时变量

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

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

更改为↓

1
2
3
4
5
6
7
8
if (area() > 1000)
return area() * 5;
else
return area() *4;
int area() {
return _length * _width;
}

引入解释性变量

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

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

更改为↓

1
2
3
4
5
6
7
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) {
......
}

移除对参数的赋值

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

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

更改为↓

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

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

重构类

搬移方法

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

1
2
3
4
5
6
class Class1 {
aMethod();
}
class Class2 {
}

更改为↓

1
2
3
4
5
6
class Class1 {
}
class Class2 {
aMethod();
}

搬移字段

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

1
2
3
4
5
6
class Class1 {
aField;
}
class Class2 {
}

更改为↓

1
2
3
4
5
6
class Class1 {
}
class Class2 {
aField;
}

提炼一个新类

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

1
2
3
4
5
6
7
class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String getTelephoneNumber() { ..... }
}

提炼到新的类中↓

1
2
3
4
5
6
7
8
9
10
11
class TelephoneNumber {
private String areaCode;
private String number;
public String getTelephoneNumber() { ..... }
}
class Person {
private String name;
private TelephoneNumber _officeNumber;
}

上面这种提炼类不一定就是合适的方式,有时候一个类不再有足够的价值的时候,我们就需要考虑提炼类的反向操作了。将类变为内联类了。

内容移动

有时候每一个子类都有声明一个字段或方法,但是父类里面却没有这个字段或方法,这时候就考虑把这个字段或方法移动到父类里面,去除子类的这个字段和方法。相反情况,如果父类有一个字段或方法,但只是某个子类需要使用,就需要考虑吧这个字段或方法移动到这个特定的子类里面了。

提炼接口

接口也就是协议,现在比较推崇的是面向接口编程。有时候接口将责任分离这个概念能发挥的淋漓尽致,把某些特性功能的方法提炼到接口中也是比较好的做法,这样其他想要这种功能的类只需要实现这个接口就行了。

重新组织数据

自封装字段

在一个类中访问自己的字段是不是应该把字段封装起来呢?这个每个人的观点是不一样的,把字段封装起来的好处就是:如果子类复写这个字段的getter函数,那么可以在里面改变这个字段的获取结果,这样子扩展性可能会更好一点。

1
2
3
4
private int _length. _width;
public int area() {
return _length * _width;
}

更改为↓

1
2
3
4
5
6
7
private int _length. _width;
public int area() {
return getLength * getWidth();
}
int getLength() {return _length;}
int getWidth() {return _width};

以对象取代数值

随着开发的进行,有时候一个数据项表示不再简单了,比如刚开始只需要知道一个人的名字就行了,可是后来的需求变成了不但要知道这个人的名字还要知道这个人的电话号码,还有住址等。这个时候就需要考虑将数据变成一个对象了。

1
2
3
class Order {
private String name;
}

更改为↓

1
2
3
4
5
6
7
8
9
class Order {
private Person person;
}
class Person {
private String name;
private String tel;
private String addr;
}

我们有时候需要把Person写成单利类,因为一个Person对象可以拥有很多份订单,但是这个对象只能有一个,所以Person我们应该写成单利。但有时候换成其他场景我们不能把他写成单利。这都是要视情况而定的。随意写代码要小心谨慎。

简化条件表达式

分解条件表达式

有时候看着一个if else语句很复杂,我们就试着把他分解一下。我想不出好的例子了,就简化一下了,各位莫怪。

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

更改为↓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

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

合并条件表达式

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

1
2
3
4
5
double disabukutyAmount() {
if (_seniority < 2) return 0;
if (_monbtdiable > 12) return 0;
if (_isPartyTime) retutn 0;
}

更改为↓

1
2
3
4
5
6
7
double disablilityAmount() {
if (isNotEligibleForDisability()) return 0;
}
boolean isNotEligibleForDisability() {
return _seniority < 2 || _monbtdiable > 12 || _isPartyTime;
}

合并重复的条件片段

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

1
2
3
4
5
6
7
if (isSpecialDeal()) {
total = price * 0.95;
send();
} else {
total = price * 0.98;
send();
}

更改为↓

1
2
3
4
5
6
if (isSpecialDeal())
total = price * 0.95;
else
total = price * 0.98;
send();

以卫语句取代嵌套表达式

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

1
2
3
4
5
6
7
8
if (a > 0) result = a + b;
else {
if (b > 0) result = a + c;
else {
result = a + d;
}
}
return result;

更改为↓

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

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

以多态取代switch语句

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

1
2
3
4
5
6
7
int getArea() {
switch (_shap)
case circle:
return 3.14 * _r * _r; break;
case rect;
return _width + _heigth;
}

更改为↓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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语句了。

然后我们还可以在一个方法中引入断言,这样可以保证函数调用的安全性,让代码更加健壮。

简化函数调用

首先要说明的是函数命名一定要有意思,一定要有意思,一定要有意思,重要的事情说三遍,不要随便命名,命名一个函数或者方法的时候一定要能表明这个方法是干什么的。

将参数对象化

函数或方法最好不要有太多的参数,太长的参数难以理解,容易造成前后不一致,最好将参数对象化,传入一个对象而不是几个参数。

1
public void amountReceived(int start, int end);

该更为↓

1
public void amountReceived(DateRange range);

当然现在参数还是比较少,你可能看不出很多好处,但是一旦参数比较多的话,你就能看出好处了,将多个参数变成了一个参数。

总结

下面是我做的一些小注意点的笔记,在文章的末尾顺便粘贴一下,看看加深一下脑子的印象。

  1. 当添加功能变得比较难的时候,就应该重构代码,先重构代码然后添加功能,重构代码应该一小步一小步的走。
  2. 方法要放到合适的类里面,找到自己合适的位置
  3. 尽量去除多余的临时变量
  4. 把大方法分割为很多小方法,函数内容越小越容易管理。
  5. 尽量使用多态。
  6. 不要有过长的参数,和过大的类
  7. 重构时修改接口,要保留旧接口,并让旧借口调用新接口。
  8. 出现switch就考虑使用多态来替换了。
  9. 尽可能的把大函数提炼成不同的小函数
  10. 有时候尽量使用内联函数
  11. 将一些临时变量用函数代替
  12. 当if语句中的判断表达式很多的时候,考虑使用临时变量分解
  13. 临时变量不应该赋值超过一次,应该使用final表示
  14. 移除对参数的改变,参数传进函数中不应该被改变本身的值
  15. 有些难以提炼的函数可以考虑使用函数对象
  16. 代码尽量不要过多出现if else语句

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

我还记得我们团队老大曾经说过,看一个人的编程水平看他的代码就足够了,可见你的代码写的好坏对你的影响非常大,那么为了我们离大牛越来越近,赶紧去重构一下你写的代码吧!!!

热评文章