重构知识点总结

什么是重构

在不改变代码本身功能的前提下,对代码做出修改,以改进程序的内部结构。
重构就是在代码写好之后改进它的设计。

在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量。

为什么要重构

避免过度设计。

防止代码腐朽。

无法预见未来的需求,随着项目推荐,重构不可避免。

不让代码变得越来越难以维护。

重构以提升可读性,可维护性,可测试性。

重构过程中,也可能找到bug。

从长期角度看,重构对项目的整体开发速度有正面影响。

没有人可以一口气就写出完美的文章,代码也一样,千锤百炼,改稿才可变得更好。

什么时候重构

  1. 代码评审,群策群力
  2. 日常开发,随时随地思考是否需要重构。

重构流程

依托单元测试,按照重构方法,进行修改,修改后逐一验证测试。

虽然现实是大部分需要重构的方法,都不具备可测试性,但仍然可以根据其行为,结合代码及需求,分析并编写单元测试。

有条件的情况下可以再加上自动化测试,资源充沛下最好安排人力回归测试。

代码坏味道

缺乏业务含义的命名

坏味道

  1. 不精准的命名
  2. 用技术术语命名

命名要能够描述出这段代码在做的事情。

一个好的命名应该描述意图,而非细节。

针对业务代码,命名不应用具体的细节,好的命名体现做什么,而不是怎么做。
针对技术代码,技术术语就是业务本身。

依赖混乱

业务代码出现具体的实现类,会导致可测试性降低,本质是违反了依赖导致原则。
代码应该朝着稳定的方向依赖(落实的代码,就是依赖抽象)

此外,maven版本依赖也容易混乱,应用dependency management管理团队之间依赖。

不一致
  1. 命名不一致
    同一个业务含义,多个单词,应建立团队共识的业务单词表。
  2. 方案不一致
    同样的目的,例如格式化时间的工具类,不应该乱七八糟的实现,应统一。
  3. 代码不一致
    一段函数内,一块解释业务,一块描述细节,层级不同,看着混乱。
重复代码

问题:
重复代码过多,修改一处,处处都得手动修改。思考一个程序,十个地方都是复制黏贴的同样代码,如果这个代码需要修改,则需要找到这十个地方一个个修改。

  1. 同一个类的两个方法都含有相同的部分代码。
    建议抽象出来一个代码。

  2. 不同的类的两个方法都含有相同的部分代码。
    考虑是否抽象出一个类将该部分代码作为单独方法,或将相同代码放在其中一个类中被另一个类调用。

  3. 同一个父类的子类的两个方法都含有相同的部分代码。
    考虑将其用模板方法模式提取出来。

减少重复代码,只需修改一处即可,防止漏掉需要修改的地方并且可以提升工作效率。

注意:不是说完全一样才是重复,要做的事情一模一样,变量名不一样也算重复,也要合并。

过长函数

问题:
一个方法的长度过长,可读性,可测试性都严重降低。
(数学计算等算法函数除外,因为复杂性不在于函数长度,而在于其本身)

应将方法中的代码分块合并,提取方法。

分块技巧:
1.找注释
2.条件表达式
3.循环

提取出的方法,方法名建议具备解释性,可替代注释的作用。上述分块技巧找到的三种代码,其代码本身并不具备解释性。

过大的类

几千行的类。

问题:

  1. 不满足单一职责原则:经常要被修改
  2. 很多类都要依赖该类
  3. 可能同时被多人修改,经常要解决代码合并冲突
  4. 难以测试
  5. 可读性,可维护性差

解决方法:拆成多个类。根据业务逻辑,或思考将使用相同实例变量的方法提取出类(一般这种就说明这俩类完全可以独立拆开运行)。

过长参数列表

太长的参数让人难以理解,阿里规范也说不要超过五个参数。

如果一定要传特别多,建议抽象成一个参数类。参数类尽量不要有该函数不需要的参数,否则会出现阅读该参数,却不知道哪些是必要,哪些是不必要。

发散式变化/反单一职责

每个对象应该只因一种变化而需要修改。其实就是单一职责原则。

霰弹式修改/四处修改

需要修改一个功能时,需要修改的代码,到处都是。

思考相关的代码是否应在一个包,一个类下。防止丢三落四。

将总是一起变化的东西放在一块。

依恋情节/过于依赖

方法依赖某个类,比依赖自己这个类还多得多。

思考这个方法是不是放错地方了。是不是可以放到依赖更多地类里。

数据泥团

多个类都有 多个同样的变量,多个方法都有同样的方法签名,入参。

思考,这同样的是否可以抽象出一个类。

基本类型偏执

同时需要几个基本类型才可表达的含义的场景,不要孤零零的放着,建议抽象出小对象

例如:
时间范围类,包含开始时间和 结束时间。
邮政编码类,包含对应地址名称和编码。

switch / if else

本质是防止过多的条件判断,导致代码更偏向于面向过程,以及代码过多问题,考虑能否用策略模式(多态),更面向对象解决。

平行继承

为某个类增加一个子类,同时必须为另一个类也增加一个子类。

解决:让一个继承体系中的实例,引用另一个继承体系的实例。

冗赘类/不需要的类

原先有价值,但重构后可有可无,建议删除,留着只会让其他人看着蒙圈。

过度设计

YAGNI 原则

You Ain’t Gonna Need It。你不会需要它

从要不要做的角度去思考

01.不要去设计当前用不到的功能。
02.不要去编写当前用不到的代码。
03.核心思想就是:不要做过度设计。

防止过度设计。

特定属性

类中的实例变量都服务于整个类,如果某个变量只给某种特定情况,建议抽象出单独的类。

不过一般情况下,类本身变量不多,且含义明确,可以忽略。

过度耦合的消息链/火车代码

本质是缺乏封装。

a.getA().getB().getC() 过长,紧密耦合,对象间关系的变化都可能导致多处调整。

本质是违反迪米特法则。必须得了解如何获得C。

可以隐藏复杂的调用消息链表,a 对象上直接提供获取 c 的方法。

最后改为 a.getC();

中间人

某个类的方法,有一半的方法都委托给其他类。出现过度委托。

有一些委托是必要的,例如委托给第三方jar提供的类对象,做隔离层,这种甚至达到100%委托。

根据实际业务,考虑是否要直接依赖真正负责的对象。

狎昵关系

过于依赖互相的私有成员。常见就是继承关系,子类过于依赖父类。例如任何方法的变化都得看一下父类实现。本质上杂糅成了一个类,还得来回切换。

考虑拆成两个类,使用组合替代依赖。

不完美的类库

不应该直接依赖第三方的类库,应该做一个隔离层/防腐层,使用组合的思想,便于修改与替换。

过多的注释

方法体内的代码如果需要依赖注释解释意图,尝试将这部分大妈抽出方法,让方法名应直接体现代码做什么,而不是过于依赖注释,注释过多很可能出现过时的注释问题,只修改了代码而忘记修改注释,导致其他人看的很迷惑。

重构方法清单

函数相关

函数本身及调用时的建议

提炼函数

将一段代码提取成一个单独的函数方法,让函数名解释其用途。

当出现如下时,常用提炼函数

  1. 过长函数
  2. 需要注释才能理解的一段代码

短小函数优点

  1. 函数被复用的机会更大
  2. 函数本身可读性很强,看高层函数时,更清晰
  3. 修改更容易
内联函数

某短代码本身可读性就很强,没必要单独抽出一个函数。
应该将这种函数去掉,直接将其内代码,放到高层函数中。
不过如果,该条件复用较多,且后续可能会变化策略,建议还是抽出函数。

优点:减少不必要的函数,降低方法层级深度,提升可读性。

int getRation() {
    return (moreThenFive()) ? 2 : 1;
}

boolean moreThenFive() {
    return this.xxx > 5;
}
// 修改如下 ↓
int getRation() {
    return this.xxx > 5 ? 2 : 1;
}
内联临时变量

使用表达式或函数直接替代 临时变量。

一般来说没有影响,当需要提取函数已查询取代临时变量时使用

int age = student.getAge();
return age >= 18;

// 修改为

return student.getAge() >= 18;
以查询取代临时变量

当出现临时变量保存某一表达式运算结果时,将该表达式提取为单独函数。

优点:临时变量只能在函数内使用,而修改为单独的函数,则其他函数方法也可以调用。

double price = this.count * this.basePrice;
return price * 0.5;

// 修改为

return basePrice() * 0.5;

double basePrice() {
    return this.count * this.basePrice;
}
引入解释性变量

复杂表达式结果(或其中一部分) 放入 临时变量

提升可读性

if(a.toUpperCase().indexOf("A") > -1) && b.toUpperCase().indexOf("B") > -1)) {
    //...
}

// 修改为
// 有业务含义的变量名字
int isOneCondition = (a.toUpperCase().indexOf("A") > -1);
int isTwoCondition = (b.toUpperCase().indexOf("B") > -1);

if (isOneCondition && isTwoCondition ) {
    //...
}
分解临时变量

针对每次赋值,创造一个单独新的临时变量。
可以再加一个 final 进一步提升可读性,告诉其他人,这个变量不会再变引用。

临时变量再一个函数内,多次赋值,会降低可读性。

int a = getAge();
// do something
a = getTempAge();

// 修改为
final int a = getAge();
// do something
final int ta = getTempAge();
移除对参数的赋值

不应该修改传入函数的参数的值(Java值传递)。
如需修改赋值操作,应新建一个临时变量。

提升可读性

int test(int param) {
    if(param >10) {
        param--;
    }
}

// 修改为

int test(int param) {
    int tparam = param;
    if(param >10) {
        tparam--;
    }
}
以函数对象取代函数

一个函数内有很多局部变量,如果拆成多个方法的话,每个方法都要传一次这么多局部变量。
可以将这些局部变量抽象到一个类中,作为这个类的属性,然后在这类里,去拆方法,这样这些拆出来的方法就可以不用参数传递。

小类,小函数,低参数,提升可读性,可维护性

double price() {
    int a, b, c;
    // do something
}

// 修改为
class Price {
    int a; int b; int c;

    public sum() {mOne(); mTwo();};
    public mOne() {};
    public mTwo() {};
}

double price() {
    int a, b, c;
    new Price(a, b ,c).sum();
    // do something
}
替换算法

如果代码可以写的更简单,就修改成更简单的方式
这条比较简单

在对象之间搬移特性
搬移函数

搬移函数情况:

  1. 一个类有太多行为
  2. 一个类与另一个类有太多合作,更多调用,高度耦合

移动到另一个类。

简化类,提升可读性,可维护性

搬移字段

某个类中的属性,被另一个类用的更多。可以搬到新的类中。

提炼类

某个类做了应该由两个类做的事。
建立一个新类,挪移相关字段和函数。

单一职责原则的重构

将类内联化

某个类,没有做太多事情。可有可无。
将这个类的所有属性和函数,挪到求他类,删除该类。

避免过多不必要的类,反而降低可读性,跳来跳去

隐藏 委托关系

在服务类上建立高层函数类,所需所有函数方法,隐藏委托关系。

A 类 依赖 B, C ,可以考虑 A 类只 依赖 B ,B 封装 C ,提供总的方法。

移除中间人

某个类做了过多的简单委托。
让高层函数直接调用委托类。最常见就是 火车调用。

引入外加函数/引入本地扩展

隔离层
遇到无法修改的类,常见就是第三方类库。
可以建一个隔离层代理类,或者用函数方式代理修改。

重新组织数据
自封装字段

通过函数访问字段,属性

间接访问,修改更灵活。
直接访问,代码更易读。但耦合更深。

个人还是倾向于函数,便于修改,提升工作效率。

以对象取代数据值

某个属性,需要与其他数据和行为一起用。
可以将其变成对象。

更面向对象,也便有扩展。

将值对象改为引用对象

好多对象都引用同样的值。
如果希望给这个值及其他相配合的对象修改直接能影响到所有依赖这个值的对象。
确保所有对象都引用同样的引用即可。

将引用对象改为值对象

值对象特性:应该是不可变的。
如果某个引用对象,很小且不可变,而且不易管理,可以将其变成值对象。
例如某个 类中的属性抽出来 直接变成 String

以对象取代数组

一个数组,每个位置都代表不同的东西。
例如 String 数字,第一位 表示姓名,第二位表示年龄。

应该改成对象。

一般也没人这么玩,规范性,做算法可能这么写

复制 被监视数据

一些业务数据,被放到了GUI控件中,而业务服务代码还要访问这些数据。

应该将这些数据复制到业务数据中,通过监听者模式,如果修改则自动同步给GUI控件。

目的是让GUI组件 与业务逻辑分离。

单向关联改为双向关联

两个类互相依赖,互相保存依赖,方便调用。

双向关联改为单向关联

两个类之间本身是双向关联,但是其实并不需要了,则去除不必要的关联,提升可读性。

常量取代魔法数

阿里规约。魔法数表示拥有特殊意义,却不能明确表现出意义的数字。
如果多处都用,修改费劲。
替换为有含义名称的常量,可读性,可维护性都更强。

封装字段

属性不要直接 public,通过 get set 访问及修改。
数据不应直接被依赖,不好修改。

封装集合

有个函数返回一个集合。
让这个函数返回该集合的只读副本,并在这类中提供添加/移除集合元素的函数方法。
不应提供直接 set 引用的方法。

降低集合拥有者和用户之间的耦合度

以子类取代类型码

类型码,说明当前这个类是啥类型。

一个类,如果根据其属性判断,决定其行为,不如直接用子类,多态覆写具体其属性的行为。

例如,员工类,可以分为工程师,销售人员,而不要在员工类中的每一个方法都要判断一下是不是工程师,是不是销售人员。

用策略模式/状态模式取代类型码

有时候不能继承,但可以用组合的方式去修改。

策略模式/状态模式本质就是组合。也是减少大量if,用类去实现。

以字段取代子类

如果子类的差别,只是返回常量数据的函数上。还不如直接删掉,只留辅父类,在父类判断。

常量函数,会返回硬编码的值。

简化条件表达式
分解条件表达式

if 后 一堆复杂的计算表达式,每一个都分解拆成单独的函数。

合并条件表达式

一堆条件但都返回一样的结果。

合并为一个函数

让用意更清晰,可以让阅读代码的人明白,实际上这里就是一个条件检查,只是有多个条件要检查。

int test() {
    if(a < 2) return 0;
    if(b < 3) return 0;
    if(c < 4) return 0;
}

// 修改为

int test() {
    if(condition()) return 0;
}
合并重复的条件片段

if else 中每一段都有一样的代码,可以抽出来,直接放到 if else 外边。

不管什么条件都调用,还留在里边也没意义。

移除控制标记

不要用控制标记。

以卫语句取代嵌套条件表达式

火箭式编程,大量缩进。

函数中的条件 让人难以看清,正确的执行路径。

可以反向思考,不满足条件则直接 return 。

比较经典

以多态取代条件表达式

多子类,或者策略模式,减少一堆if 判断。

引入Null对象

减少大量if null 判断,但是得额外思考,很多时候没必要这么搞

Java8还提供了一个 Optional 的工具,提醒处理空指针逻辑。

引入断言

相当于前置校验,不满足则抛出异常,断言不太常用,更常用的是Guava的 Preconditon。

简化函数调用
函数改名

修改函数名以达到可以描述函数用途。

去除不必要的维护注释成本。

查询和修改分离

不要同时又查又改

令函数携带参数

若干个函数做了类似工作,且完全可以传入一个简单参数控制。

getFive()
getTen()

// 修改为

get(int param)
以明确函数取代参数

函数内不要 if 判断参数走 一段逻辑,if 走另一端,这种可直接根据含义建立两个函数。

保持对象完整

如果传参的时候,是穿进去多个 A 对象.get 出的各个属性,不如直接给 A 对象作为参数传递进去。

以函数取代参数

如果函数可以通过其他途径获得参数,就不要作为参数传递进来。过长的参数会在降低可独行。不过如果这个参数要在之后也多次使用,且计算成本很高,性能慢,还是可以传递。

移除设值函数

如果不希望某个类创建后,还允许修改其中的值,就删掉set,告诉别人,不可改。

隐藏函数

不需要别人调用的方法,就设置为 private,不暴露更多方法出去。

工厂取代构造

不要new对象,紧耦合,可测试性也不行,也不好修改,一般用SpringBoot依赖注入更多一点,这个目前还好。

封装向下转型

强转操作,放到被调用的函数中,而不是高层函数。

以异常取代错误码

参考Guava的Precondition 结合 自定义异常,防止大量不必要的高层函数 调用后,再if else 判断。

以测试取代异常

意思是尽可能的提前判断数据是否满足要求,判断边界条件等,而不是让异常抛出。这个和上边的以异常取代错误码,并不冲突。都是希望提前做校验。

处理概括关系
字段上移

两个子类都有相同字段,将该字段移至父类。

父类某个字段,只有部分子类用,挪下去。

函数上移/下移

每个子类都有一样的方法(或目的一样,结果一样)将该函数挪到父类。

不一样,就下移。

构造函数本体上移

每个子类的一些构造函数,可能有多个重复的参数,可以挪到父类,子类调用父类。

提炼子类

某个类的某些属性,方法只有一部分情况才用到,可以针对这些建一个子类。

提炼超类

两个类相似。

建父类,相同的点,挪到父类,然后继承,复用。

提炼接口

多个类,有部分行为一样,可以抽象接口。

接口表示行为

折叠继承关系

父子类没啥区别,直接合成一个。

模板函数

一些子类,某些函数都要按相同顺序执行,只是细节不同,可以抽出父类,用模板方法模式处理。

委托取代继承

子类只需要父类的很少一部分,或者根本不需要继承来的数据。
修改为组合。

继承取代委托

如果两个类,大量的简单组合委托调用,不如直接改成继承。

IDEA重构相关快捷键

  1. 重命名文件、方法、属性等(rename):SHIFT+F6

  2. 提取当前选择为变量(extract variable):CTRL+ALT+V
    可以用于表达式 或者 给某个变量重新赋值到新变量。

public void test() {
    A.set(new Student());
}public void test() {
    Student student =  new Student();
    A.set(student);
}
  1. 提取当前选择为属性(extract field):CTRL+ALT+F

  2. 提取当前选择为常量(extract constant):CTRL+ALT+C

  3. 提取当前选择为方法(extract method):CTRL+ALT+M
    自动检测选择范围代码哪些变量作为方法入参

  4. 提取当前选择为方法参数(extract parameter):CTRL+ALT+P
    把当前方法内某变量,某个赋值方法返回值添加到当前方法的入参中

  5. 提取代码块至if、try等结构中(surround with):CTRL+ALT+T

参考

  1. 《重构:改善既有代码的设计》 Martin Fowler
  2. 《设计模式之美》王争
  3. 《代码之丑》郑烨
  4. 《软件设计之美》郑烨
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宋小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值