写代码的正确姿势-剑招之重构

这篇是即写代码的正确姿势之后的第二篇。或许你应该知道了,重构是怎样的,你也知道重构能带来什么。开发的流程大概

编写测试->功能代码->修改测试->重构

当然当逻辑很清楚的时候, 编写测试可以不用先行,反之逻辑复杂是编写测试先写。重构在我们写程序时无处不在,位置也很固定。

为什么要重构

在Martin Fowler的《重构:改善既有代码的设计》一书中强调了代码中有哪些的坏味道,并告诉我们如何重构。我们经常遇到以下情景:

有时候你可能要面对着一坨一坨的代码,有时候你可能要花几天的时间去阅读代码, 你也可能会骂:‘写的都是shit,在没别这更烂的了’。 你想重写那几十代码可能只会花上你不到一天的时间。

每到这个时候, 我们可以这样想,如果你没办法理解当时为什么这么做,你的修改只会带来更多的bug。修Bug,更多的是维护代码。只有下心来, 你会发现修复bug也是一门艺术。

重构时机

时时刻刻

重构代码最佳的时间点:撰写每行代码的时候,而非迫不得已的时候。就是Martin Fowler提到坏味道,如果你遇到下列情景其实在提醒你代码该重构了:

当你写一段代码时,不得不从别处拷贝粘贴代码?

这个应该在初学编程时候经常犯这种问题, 记得刚入职场时经常用这项技能,屡试不爽。不一些老鸟教育了几次也不敢用了。其实由于人性的懒惰,偶尔也会犯一次。现在回过头看自己早期写的程序(对, 还算不上作品)添加一点逻辑需要检查七八个地方是否需要同样的逻辑,完全可以入选教材作为经典的反面案例。

当你修改已有代码添加新功能时,发现已有代码总感觉哪里不对?

比如说,逻辑写得太绕,太复杂,太难以理解,循环太多,分支太多,状态太多等等。这样的代码几乎跪在那里请求你的重构,不重构说不过去。

当你调用已有的代码时(函数,类),不得不阅读被调用的代码才能确定怎么调用时?

Martion Fowler这样说过,如果发现代码中有注释的地方都能通过重构进行去掉,那么注释写在哪里了, 当然写在函数和类的说明上。

  1. 一个函数有十多个参数;要么是文档写的不好,

  2. 关键性的函数没有对接口提供足够的说明

重构之术

面向对象编程中的类的重构和函数式编程中的函数的重构也不尽相同;但是Java(OOP)、JavaScript(FP)它们的重构手段就千差万别了。下面主要说说用Idea重构java代码吧。

Intellij Idea重构

以前用emacs写java代码时候, 进行代码重构一般都手动进行, 也没有想去找一些插件进行重构。以后发现Intellij idea自带重构工具,解放了生产力, 妈妈再也不担心我的学习了。

看一下下边的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.jason.util;
/**
* Created by jason on 16/2/21.
*/
public class Calculate {
public int substract(int a, int b) {
return a - b;
}
public int multiplay(int a, int b) {
return a * b;
}
public int divide(int a, int b) {
return a/b;
}
public static void main(String[] args) throws Exception {
int temp = 1 + 2;
int temp1 = new Cal2().fibonacci(5);
System.out.println("add(): " + temp + " finbonacci(): " + temp1);
}
}

Rename

  1. 把光标放到temp上,按下shift+f6,输入add

  2. 把光标放到temp1上,按下shift+f6,输入add

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
int add = 1 + 2;
int fibonacci = new Cal2().fibonacci(5);
System.out.println("add(): " + add + " finbonacci(): " + fibonacci);
}

Extract method

mac: command + alt + m

选中

int add = 1 + 2;

按下command + alt + m 结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
int add = add();
int fibonacci = new Cal2().fibonacci(5);
System.out.println("add(): " + add + " finbonacci(): " + fibonacci);
}
private static int add() {
return 1 + 2;
}

Extract paramter

mac: command + alt + p

  1. 把光标放到1上,按下command + alt + p,输入a

  2. 把光标放到2上,按下command + alt + p,输入b

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws Exception {
int add = add(1, 2);
int fibonacci = new Cal2().fibonacci(5);
System.out.println("add(): " + add + " finbonacci(): " + fibonacci);
}
private static int add(int a, int b) {
return a + b;
}

Inline Method

mac: command + alt + n

选中add(),按下command + alt +n 又回到以前了

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
int add = 1 + 2;
int fibonacci = new Cal2().fibonacci(5);
System.out.println("add(): " + add + " finbonacci(): " + fibonacci);
}

Pull Members Up/Push Members Down

Pull Members Up

我们可以看到在Cal2类中有一个fibonacci()函数, 并且继承Calculate类。我们认为他应该放在Calculate类中,我们就可以进行如下方法:

Push Members Down

相反方式处理, 就可将刚才修改还原了

Replace Temp with Query

《重构:改善既有代码的设计》中这样写到:

我们都知道临时变量都是暂时的,而且只能在所属的函数中使用。所以它们会驱使你写出更长的函数,因为只有这样你才能访问到需要的临时变量。如果把临时变量替换为一个查询,那么同一个类中所有函数都将可以获得这份信息。这将带给你极大帮助,使你能够为这个类编写出更清晰的代码。

该重构方法往往是你运用提炼函数之前必不可少的一个步骤。局部变量会使得代码难以提炼,所以应该尽可能把它们替换为查询式。比较简单的情况是:临时变量只被赋值一次,或者赋值给临时变量的表达式不受其它条件影响。

用法

鼠标: Refactor | Replace Temp with Query

Martin Fowler的例子

重构前:

1
2
3
4
5
6
double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.98;

重构后:

1
2
3
4
5
6
7
8
9
if (basePrice() > 1000)
return basePrice() * 0.95;
else
return basePrice() * 0.98;
...
double basePrice() {
return _quantity * _itemPrice;
}
JasonThink wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!