重构:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本

1. 重构原则

  1. 重构应该随时随地进行,而不是专门安排时间进行重构

  2. 添加功能时重构

  3. 修补错误时重构

  4. 复审代码时重构

2. 代码的坏味道

学会判断一个类有多少实例变量算是太大,一个函数内有多少行代码才算太长

  1. 当在多个地方coding同样的代码时,应该及时抽离成函数,然后进行调用

  2. 当一行或者多行代码无法直接看出其含义时,应该抽成一个函数,并对这段代码的工作进行合理的命名

  3. 当类太大时,应该及时的把类中的函数、字段等按照各个职责、行为等进行抽离

  4. 如果一个函数的参数列太长了,应该构建一个类进行保存并作为参数传递

  5. 尽量使一个对象只由一种变化而引起变化。比如说一个类,类中包含金额字段,可以在类中增加方法对这个金额进行加减,而不是通过set方法进行直接赋值。

  6. 返回的场景,以null对象代替null值,避免出现空指针异常。然后在具体的业务逻辑中,在无法获取到对应的值时,再中断流程或跳出本次循环。

  7. 当有一部分麻烦的代码,想通过注释进行解释时,可以考虑先重构这一块,把各个代码段以函数代替,并在名称中按照动作和行为取名,看看是否能取代注释。

3. 构建测试体系

  1. 确保所有的测试都是完全自动化的,让程序检查自己的结果。

  2. 当如果有改动时,只需要运行测试程序,就能检查代码是否存在问题。如果有业务的改动,则需要把测试程序一起修改掉。

  3. 当遇到一个bug时,应该及时的编写一个单元测试来检查,并在修改后重复运行测试程序,检查是否正确。

  4. 尽量多考虑边界情况,因为边界值比普通的case更容易出现问题

4. 重构列表

  1. 构建一个重构的记录表,并且在里面写上名称、动机、做法以及对应的范例。

5. 重新组织函数

  1. 当一段代码,需要通过注释来解释这一段代码时,可以尝试将其抽离成一个函数,并以行为+动作命名。而如果其函数内的代码本身,已经可以解释在做什么的时候,则应该将函数内的代码放回原处。

  2. 当一个临时变量保存了某个表达式的结果,且在方法中多次被调用时,可以将这个表达式抽离成一个方法,然后在原方法中直接调用这个方法获取值,而不是直接用临时变量。这样子可以避免在重构这个方法的时候,临时变量也要作为参数传递,可以减少传递的字段。

  3. 减少临时变量的使用。

    1. 当一个变量只进行了一次赋值时,可以考虑以动作本身代替这个临时变量。如果这个变量代替的是一个查询方法,也可以直接将这个查询方法内联到代码中,以提高拓展性。

    2. 如果遇到了较为复杂的表达式计算,则可以考虑使用变量,以解释过程计算。同时可以结合第五部分的b小点,单独成为一个方法,那么直接用方法名来表示这个表达式也可。具体是用变量还是用方法,则根据方法内部重构时,这个临时变量是否被多次使用来判断。

    3. 要注意变量尽量只被赋值一次,避免改变其含义。即使有相同类型的数据要临时保存,应该声明一个新的变量去保存,而不是直接改变当前变量的引用,从而避免变量的含义杂乱不清。

  4. 如果存在一个函数,里面大量的使用到了局部变量,可以考虑直接使用一个类,局部变量都写成对应的字段,然后将一个大的函数,抽离成类中的各个小函数。(可以参考DDD的充血模型

  5. 减少对参数的赋值动作,应该尽量让方法返回所需要的数据。这样可以避免因为值传递和引用传递导致的问题等,也可以让方法的语义更清晰

6. 在对象之间搬移特性

  1. 一个类中的函数或者字段,被另一个类大量的使用,且这些字段和函数,并非某个类的特有属性时,可以考虑将这些字段和函数迁移到这个新类,从而减少这两个类的耦合关系。若这些字段和函数为共有特征时,可以抽出一个父类保存这些函数和字段。

  2. 应该明确本类的职责,当出现许多不属于本类的职责时,应该提取到新的类中。领域划分可以参考:DDD划分领域、子域、核心域、支撑域的目的 - 等不到的口琴 - 博客园

    1. 若一个类的职责可以被另一个类所涵盖时,则可以将较小的类放入较大的类中进行内敛化

  3. 想改造一个不可修改的类(无法继承)的部分函数时,可以考虑创建一个新的类,然后在新的类中引入这个类并进行函数拓展与改造

7. 重新组织数据

  1. 当发现一些数据项增多,且为同一种相似类型的对象时,可以用用对象来取代数据的值

  2. 如果我们使用了一个数组,并且这个数组中的类型是object,里面保存了各种不同类型的值,我们则应该将其用对象来代替,把这个数组中的各个值都转换成对象中的字段,这样子更方便理解和管理。

  3. 用常量代替魔数。

  4. 假如对象存在一个集合,且在其他方法中会对其调用并做修改,应该在这个方法中,为这个集合提供get/remove方法,由这个对象来管理这个集合,不让外部直接对集合做操作。

  5. 用子类来替代类型码,当我们拥有一些类型码,且这些类型码为某个类下的各种行为特征时,应该用子类来代替这些类型码,然后将各个类型码对应的后续动作抽离到对应的子类中,这样子就可以直接应用多态的特性,在无需很多Switch或if判断的情况下,直接对相应的子类行为做调用。(策略模式)

8. 简化条件表达式

  1. 分解复杂的条件表达式,将各个分类相似的条件表达式抽离成方法,这样子在判断中,就能直接根据各自的方法名得出这次if所需要判断的条件。

  2. 当出现连续的条件表达式,且各个表达式中,存在相同的部分,则应该将这部分表达式抽取到这些if之外,这样子就能更清楚的知道,各个表达式中哪些才是变化的因素

  3. 如果我们有一个if/else的分支,这是if和else代表的应该是并行的两个表达式,如果在其中有任何一个特殊的表达式,都应该抽取出来作为判断的判断条件。如果if/else中的各个条件是某个类下的各种行为特征,则考虑多态的特性将其解决,参考第七部分的e小结

  4. 用null对象来取代null值。这个方法可以避免我们在程序中多次对一个对象进行判空处理,具体做法是创建一个名为nullXXXX的对象,并继承原有对象,同时重写方法,方法内的动作,就是其程序获取到对应null值的处理动作。例如:我们有一个换电站对象,通过换电站对象获取对应仓内的电池数,此时我们可以考虑让这个null对象返回为0,这样子只需要在调用方,处理不同电池数的场景即可,而不用担心空指针的问题。若是程序中需要处理换电站对象查不到的情况,那么原对象和null对象都添加一个isNull方法即可解决这个问题。

9. 简化函数调用

  1. 将复杂的方法体内容,抽成各个小函数,并正确命名,做到见名知意(有点难)。若已有函数的方法名不能解释其对应动作,那么就需要对方法进行重命名。参考《阿里巴巴代码开发手册》时,上面有提到不管方法名再长,只要能解释其含义,也应该做。(但是目前我还没见过...可能还是我看的太少了)

  2. 将查询函数和修改函数分开,往往我们为了方便,都会在查询之后,直接对原有的数据进行修改,那么就会导致这个数据的维护变得困难,若是我们将查询得到的数据设置为不可变,并提供一个赋值函数时,那么这个数据的变化,都会由对应的对象来控制了,数据的可控性会更高。

  3. 要注意保持对象的完整性,如果某个子方法使用了对象的部分字段,往往我们会将这个字段单独传到子方法,而如果我们我们在子方法,使用到的新的字段,则又要在方法中增加新的参数,容易导致参数过长。直接传递对象则可以避免这个问题。当然,这也会造成方法与对象的强依赖关系,若是分析后我们的方法不会时常新增新的函数,则可以考虑新增一个DTO对象作为参数传递。

  4. 如果对象调用了某个函数得到一个结果,并将这个结果传给下一个函数,如果函数本身也可以通过直接调用的方式获得这个结果时,此时应该考虑消除这个参数,改为直接调用对应的方法获取值,这样子可以消除过长的参数。(考虑是否存在性能问题)

  5. 封装向下转型的过程,如果我们需要获取某个类的子类时,而对应的方法,又是返回了其父类,我们应该在这个类中增加获取子类的方法,转型的动作由其自身完成,而不是由调用方完成。

  6. 用异常代替错误码,我们要明确一段代码发生异常时,这个异常的数据是可控的还是非可控的。假设数据是我们预期的异常数据,那么可以返回错误码,而如果是非预期的数据,那么就应该抛出异常而中断流程。(指的是后端流程,与前端的交互还是要看双方协定哈)

10. 处理概括关系

  1. 若是所有子类都拥有相同的字段,此字段应该上移到父类中。若是某些子类不包含这个字段,则不上移

  2. 若是子类的构造函数与父类也完全相同,则应该上移到父类,子类直接调用父类的构造函数

  3. 若是两个类有完全相同的相似的行为,则可以为两个类建立一个父类,将相似的行为抽离到父类中。若是各自子类的这些共有行为发生了改变,则应该由子类各自重写对应的逻辑部分。

  4. 当有多个子类,且这些子类相应的函数中,都以相同的顺序执行类似的操作,只是操作细节上有所不同,则可以将这些行为抽离成一个共有的父类函数,由父类决定执行顺序,操作细节由各子类内部完成。(模板方法模式)

  5. 如果某个子类只使用了父类的一部分接口,或是不需要使用继承来的大部分数据,可以用组合代替继承,让两者之间的依赖关系减弱。如果用到了父类的全部接口,那么还是应该使用继承,以此来表示两者间的强烈依赖关系。

11. 大型重构

  1. 梳理某个继承关系下的责任,每个继承体系应该只承担一个责任,将其他的责任分离成新的类,创建新的继承体系。

  2. 领域划分,梳理程序的整体架构上,细分为哪些领域模块,将各个模块的关系进行表述与分离。领域划分不同,那么各个类的职责也不相同。举例:MVC模式划分领域,业务对象划分领域(参考DDD的领域划分:DDD划分领域、子域、核心域、支撑域的目的 - 等不到的口琴 - 博客园