重构-改善代码已有的设计

如果没有单元测试和重构,我没办法写代码

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
2
3
4
5
6
void createOrder() {
check();
calculate();
pay();
log();
}
  • 让 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
    10
    function 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
    15
    double 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
    3
    public abstract class MovieBase {
    public abstract void ChargeFor();
    }
    1
    2
    3
    4
    5
    6
    public 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
if ("3".equals(emndVo.getStatus())) {}
1
if (allId.size() <= 500) 

优化

  • 新建个常量
  • 建一个枚举类,把相关的魔法数字放到一起管理。

5.12

6 其他提升代码可读性的方式

6.1 Introduce Explaining Variable(引入解释性变量)

  • 将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
  • 表达式有可能非常复杂而难以阅读。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。

6.2 使用pair

7 构建单元测试


重构-改善代码已有的设计
http://example.com/重构-改善代码已有的设计/
作者
Panyurou
发布于
2022年4月14日
许可协议