概念

重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。

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

目的

  1. 保证代码的质量,避免代码的增加导致混乱,以至于影响功能的增加,难以发现bug,难以维护;
  2. 替换更好的框架设计,随着业务变更最开始设计的框架会逐渐的不适应、不满足新的需求;
  3. 避免过度设计,前期可以减少精力在大量的/过度的设计,而是在遇到问题时再去逐步重构,以满足需求;
  4. 提升工程师的能力,可以设计,写出更好的框架设计,更合理,低耦合高内聚的代码

对象

重构的对象可以根据规模来分为两类,大规模的和小规模的。

大型重构指的是对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入 bug 的风险也会相对比较大。

小型重构指的是对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名、规范注释、消除超大类或函数、提取重复代码等等。小型重构更多的是利用我们能后面要讲到的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入 bug 的风险相对来说也会比较小。你只需要熟练掌握各种编码规范,就可以做到得心应手。

时机

最好的时机是在平时的工作中进行持续的小型的重构,在一些比较大的影响/当前业务繁重时/当前没什么新的需求时做一些大的重构。

也因为如此,不要在设计初就考虑的情况过多,而造成过度的设计,也不要在设计初完全的遵守xxxx的守则,项目的架构设计和当前的规模、需求是分不开的,随着业务的变迁,要逐步的重构,以满足需求。

单元测试

用来保证重构后的代码未改变软件的可见行为。

单元测试由研发工程师自己来编写,用来测试自己写的代码的正确性。我们常常将它跟集成测试放到一块来对比。单元测试相对于集成测试(Integration Testing)来说,测试的粒度更小一些。集成测试的测试对象是整个系统或者某个功能模块,比如测试用户注册、登录功能是否正常,是一种端到端(end to end)的测试。而单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试。

单元测试除了能有效地为重构保驾护航之外,也是保证代码质量最有效的两个手段之一(另一个是 Code Review)。

单元测试的好处

  1. 单元测试能有效地帮你发现代码中的 bug
  2. 写单元测试能帮你发现代码设计上的问题
  3. 单元测试是对集成测试的有力补充
  4. 写单元测试的过程本身就是代码重构的过程
  5. 阅读单元测试能帮助你快速熟悉代码
  6. 单元测试是 TDD 可落地执行的改进方案(TDD:测试驱动开发,测试用例先写)

写单元测试需要了解代码的实现逻辑吗

单元测试不要依赖被测试函数的具体实现逻辑,它只关心被测函数实现了什么功能。我们切不可为了追求覆盖率,逐行阅读代码,然后针对实现逻辑编写单元测试。否则,一旦对代码进行重构,在代码的外部行为不变的情况下,对代码的实现逻辑进行了修改,那原本的单元测试都会运行失败,也就起不到为重构保驾护航的作用了,也违背了我们写单元测试的初衷。

高耦合代码

如果一个类职责很重,需要依赖十几个外部对象才能完成工作,代码高度耦合,那我们在编写单元测试的时候,可能需要 mock 这十几个依赖的对象。不管是从代码设计的角度来说,还是从编写单元测试的角度来说,这都是不合理的。

实现“高内聚,低耦合”

单一职责原则

我们前面提到,内聚性和耦合性并非独立的。高内聚会让代码更加松耦合,而实现高内聚的重要指导原则就是单一职责原则。模块或者类的职责设计得单一,而不是大而全,那依赖它的类和它依赖的类就会比较少,代码耦合也就相应的降低了。

基于接口而非实现编程

基于接口而非实现编程能通过接口这样一个中间层,隔离变化和具体的实现。这样做的好处是,在有依赖关系的两个模块或类之间,一个模块或者类的改动,不会影响到另一个模块或类。实际上,这就相当于将一种强依赖关系(强耦合)解耦为了弱依赖关系(弱耦合)

依赖注入

跟基于接口而非实现编程思想类似,依赖注入也是将代码之间的强耦合变为弱耦合。尽管依赖注入无法将本应该有依赖关系的两个类,解耦为没有依赖关系,但可以让耦合关系没那么紧密,容易做到插拔替换。

多用组合少用继承

我们知道,继承是一种强依赖关系,父类与子类高度耦合,且这种耦合关系非常脆弱,牵一发而动全身,父类的每一次改动都会影响所有的子类。相反,组合关系是一种弱依赖关系,这种关系更加灵活,所以,对于继承结构比较复杂的代码,利用组合来替换继承,也是一种解耦的有效手段。

迪米特法则

迪米特法则讲的是,不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。从定义上,我们明显可以看出,这条原则的目的就是为了实现代码的松耦合。

代码规范

类中成员的排列顺序

先静态,后普通。
作用范围大、作用范围小(private)。

不用函数的参数来控制逻辑

避免if条件为true时走一个大块的逻辑,为false的时候走另一个大块的逻辑,可以把这些逻辑放到单独的一个函数中,以免违背单一职责原则和接口隔离原则。

函数的功能要单一

函数的粒度相对较小,要满足单一职责原则。尽可能的功能单一。

移除过深的嵌套

去掉多余的if-else:比如在if中就返回了,那么if外没必要增加else;
使用continue、break来避免在if中写大段代码块;
将部分嵌套的逻辑封装成函数来使用;

使用解释性变量

避免魔法数;
代替复杂的逻辑判断表达式,使其判断条件简单明了;

发现代码中的问题

image.png
image.png