重构-改善代码已有的设计
如果没有单元测试和重构,我没办法写代码
1 重构的意义
- 保持代码易读、易修改
- 避免代码太复杂,无法理解,无法调试。(或许一个修改只需要10分钟,但是你得花费1个小时去理解这段代码
- 如果没有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在 调试上面,无法添加新功能。修改时间越来越长,因为你必须花越来越多的时间去理解系统。
2 重构的定义
在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。
3 重构的前提
为即将修改的代码建立一组可靠的测试环境。
- 因为尽管遵循重构手法可以使我避免绝大多数引入bug的情形,但我毕竟是人,毕竟有可能犯错。所以我需要可靠的测试。
好的测试是重构的根本。
4 何时重构
4.1 添加新功能时重构
此时,重构的直接原因往往是为了帮助我理解 需要修改的代码——这些代码可能是别人写的,也可能是我自己写的。
无论何时,只要我想理解代码所做的事,我就会问自己:是否能对这段代码进行重构,使我能更快地理解它。然后我就会重构。
之所以这么做,部分原因是为了让我下次再看这段代码时容易理解,但最主要的原因是:如果在前进过程中把代码结构理清,我就可以从中理解更多东西
4.2 复审代码时重构
这种活动有助于在开发团队中传 播知识,也有助于让较有经验的开发者把知识传递给比较欠缺经验的人,并帮助更多人理解大型软件系统中的更多部分。
4.3 修补错误时重构
代码还够清晰——没有清晰到让你能一眼看出bug。
5 代码坏味道
5.1 命名规范
错误方式
单词拼写错误
自己创造缩写
使用技术术语命名
如:userList , idList
编程的一个重要原则是面向接口编程。即接口是稳定的,实现是易变的。
假设这里我现在需要的是一个不重复的作品集合,也就是说这里需要把List改成Set,变量类型一定会改,但是你不一定会记得改变量名,一旦遗忘,就会出现一个bookList变量,它的类型是set,就会产生混淆。
命名中的不一致。
优化
- 命名要有业务含义
- 建立团队的词汇表,让团队成员有信息可以参考。
- 制定代码规范,比如,类名要用名词,函数名要用动词或动宾短语;
- 符合英语语法规则
- 类似含义的代码应该有一致的名字,一旦出现了不一致的名字,通常都表示不同的含义
5.2 Duplicated Code(重复代码)
- 复制粘贴的代码
- 部分重复
- if 和 else 代码块中的语句高度类似。
优化
抽方法
Template Method(模版方法): 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中
1 |
|
- 让 if 语句做真正的选择
5.3 Long Method(过长函数)
产生原因
把代码平铺直叙地摊在那里
一次加一点
缺点
程序愈长愈难理解
把多个业务处理流程放在一个函数里实现;
把不同层面的细节放到一个函数里实现。
优化
- 如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数去。
- 循环常常也是提炼的信号。你应该将循环和其内的代码提炼到一个独立函数中。
- 建议30 行,idea屏可以看得完
5.4 Large Class(过大的类)
缺点
类内如果有太多代码,也是代码重复、混乱并最终走向死亡的源头
优化
- 类的职责保证单一
- 字段未分组。比如,userId、name、nickname 几项,算是用户的基本信息,而 email、phoneNumber 这些则属于用户的联系方式
5.5 Long Parameter List(过长参数列)
缺点
- 太长的参数列难以理解,太多参数会造成前后不一致、不易使用
- 如果有多个重载方法,参数很多的话,有时候你都不知道调哪个
优化
- 将参数封装成结构或者类
5.6 Shotgun Surgery(霰弹式修改)
缺点
- 每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你不但很难找到它们,也很容易忘记某个重要的修改。
优化
- 把所有需要修改的代码放进同一个类
5.7 Data Clumps(数据泥团)
数据泥团指的是经常一起出现的数据, 比如:
- 两个类中相同的字段
- 多个函数签名中相同的参数
优化
- 提炼到一个独立对象
5.8 Switch Statements(switch惊悚现身)
- 少用switch(或case)语句
缺点
- switch语句的问题在于重复。你常会发现同样的switch语句散布于不同地点。
- 如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们
优化
构建map
1
2
3
4
5
6
7
8
9
10function getDeliverResult(status) {
switch (status) {
case 1:
return '待发货';
case 2:
return '已发货';
default:
return '';
}
}1
2
3
4
5
6
7
8
9
const orderStatus = new Map()
.set(1, '待发货')
.set(2, '已发货')
.set(3, '已完成');
function getOrderStatus(statusCode) {
return orderStatus.get(statusCode) || [];
}多态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15double result = 0;
void ChargeFor(){
switch(PriceCode){
case Movie.REGULAR:
result = (new RegularPrice()).ChargeFor(daysRented)
break;
case Movie.REGULAR:
result += daysRented * 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) * 1.5;
break;
}1
2
3public abstract class MovieBase {
public abstract void ChargeFor();
}1
2
3
4
5
6public class RegularMovie extends MovieBase{
@Override
public void ChargeFor() {
return new RegularPrice()).ChargeFor(daysRented);
}
}
5.9 过深的if else嵌套
圈复杂度不能超过6,3个if else
缺点
- 看起来很复杂
优化
- 抽方法
- 使用多态
5.10 过多的注释
缺点
很多注释的存在是因为代码很糟糕
优化
方法函数、变量的命名要规范、浅显易懂、避免用注释解释代码。
关键、复杂的业务,使用清晰、简明的注释
5.11 magic变量
缺点
- 看了令人迷惑
如:
1 |
|
1 |
|
优化
- 新建个常量
- 建一个枚举类,把相关的魔法数字放到一起管理。
5.12
6 其他提升代码可读性的方式
6.1 Introduce Explaining Variable(引入解释性变量)
- 将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
- 表达式有可能非常复杂而难以阅读。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。