重新组织函数
Tip
最好能把大的修改拆成小的步骤,所以如果你既想修改函数名,又想添加参数最好分成两步来做。
不论何时,如果遇到了麻烦,请撤销修改,并改用迁移式做法)
中文名 | 英文名 | 时机 | 做法 | 意义 | |
---|---|---|---|---|---|
变量改名 | Rename Variable | 1 变量/常量的名字不足以说明字段的意义 2 垃圾命名 3 如果在另一个代码库中使用了该变量,这就是一个"已发布变量"(published variable),此时不能进行这个重构。 | ①先用封装变量手法封装 ②找到所有使用该变量的代码,修改变量名 ③测试 ④只作用于某个函数的直接替换即可 ⑤替换过程中可以以新名字作为过渡。待全部替换完毕再删除旧的名字 | 表意准确 | |
以查询取代临时变量 | Replace Temp with Query | 1 修改对象最好是一个类(这也是为什么提倡class,因为类可以开辟一个命名空间,不至于有太多全局变量) 2 有很多函数都在将同一个值作为参数传递 3 分解过长的冗余函数 4 多个函数中重复编写计算逻辑,比如讲一个值进行转换(好几个函数内都需要这个转换函数) 5 如果这个值被多次修改,应该将这些计算代码一并提炼到取值函数 | ①检查是否每次计算过程和结果都一致(不一致则放弃) ②如果能改为只读,就改成只读 ③将变量赋值取值提炼成函数 ④测试 ⑤去掉临时变量 检查变量在使用前是否已经完全计算完毕,检查计算它的那段代码是否每次都能得到一样的值。 if (isCalcXx) | 可读性 | |
封装变量 | Encapsulate Variable | 1 当我们在修改或者增加使用可变数据的时候 2 数据被大范围使用(设置值) 3 对象、数组无外部变动需要内部一起改变的需求时候,最好返回一份副本 | ①创建封装函数(包含访问和更新函数) ②测试 ③控制变量外部不可见(private、protected) ④测试 | 不可变性是强大的代码防腐剂。 数据被使用得越广,就越是值得花精力给它一个体面的封装。 | |
拆分变量 | Split Variable | 1 一个变量被应用到两种/多种的作用下 2 修改输入参数的值 | 【重复】{ ①在变量第一次赋值的地方,为函数取一个更加有意义的变量名(尽量声明为const) ②在第二次赋值地方声明该变量 ③以该变量第二次赋值动作为界,修改此前对该变量的所有引用。让他们引用新的变量 ④测试} | 让代码更容易读懂 | |
封装记录 | Encapsulate Record | 1 可变的记录型结构 2 一条记录上有多少字段不够直观 3 有需要对记录进行控制的需求(个人理解为需要控制权限、需要控制是否只读等情况) 4 需要对结构内字段进行隐藏 | ①首先用封装变量手法将记录转化为函数(旧的值的函数) ②声明一个新的类以及获取他的函数 ③找到记录的使用点,在类内声明设置方法 ④替换设置值的方法 ⑤声明一个取值方法,并替换所有取值的地方 ⑥测试 ⑦删除旧的函数 ⑧当我们需要改名时,可以保留老的,标记为不建议使用,并声明新的名字进行返回 | 获取与修改分离 | |
提炼变量 | Extract Variable | 1 一段又臭又长的表达式 2 在多处地方使用这个值(可能是当前函数、当前类乃至于更大的如全局作用域) 3 如果这个变量名在更宽的上下文中也有意义,我就会考虑将其暴露出来,通常以函数的形式。 | ①确保要提炼的表达式,对其他地方没有影响 ②声明一个不可修改的变量,并用表达式作为该变量的值 【重复】{ ③用新变量(或函数)取代原来的表达式 ④测试 } | 局部变量可以帮助我们将表达式分解为比较容易管理的形式 | |
内联变量 | Inline Variable | 1 变量没有比当前表达式有什么更好的释义 2 变量妨碍了重构附近代码 3 有一个临时变量,只被一个简单表达式赋值一次 | ①检查确认变量赋值的右侧表达式不对其他地方造成影响 ②确认是否为只读,如果没有声明只读,则要先让他只读,并测试 【重复】{ ③找到使用变量的地方,直接改为右侧表达式 ④测试 } ⑤删除该变量的声明点和赋值语句 ⑥多个内联变量在一起,可以用提炼函数取代临时变量 | 简化调用链 | |
提炼函数 | Extract Function | 1 当一段大函数内某一部分代码在做的事情是同一件事,并且自成体系,不与其他掺杂时 2 当代码展示的意图和真正想做的事情不是同一件时候 3 如果变量按值传递给提炼部分又在提炼部分被赋值,就必须多加小心 4 如果想不出一个更有意义的名称,这就是一个信号,可能我不应该提炼这块代码。 | ①以他要做什么事情来命名的函数待提炼代码复制到这个函数 ②检查这个函数内的代码的作用域、变量 ③编译查看函数内有没有报错 ④替换源函数的被提炼代码替换为函数调用 ⑤测试 ⑥替换其他代码中是否有与被提炼的代码相同或相似之处 | 将意图与实现分开 | |
内联函数 | Inline Function | 1 函数内代码直观表达的意思与函数名字相同 2 有一堆杂乱无章的代码需要重构,可以先内联函数,再通过提炼函数合理重构 3 非多态性函数(函数属于一个类,而这个类被继承) | ①检查多态性(如果该函数属于某个超类,并且它具有多态性,那么就无法内联) ②找到所有调用点 ③将函数所有调用点替换为函数本体(非一次性替换,可以分批次替换、适应新家、测试) ④删掉该函数的定义(也可能会不删除,比如我们放弃了有一些函数调用,因为重构为渐进式,非一次性) ⑤检查上下文有没有导致重复的变量名 | 间接性可能带来帮助,但非必要的间接性总是让人不舒服 | |
改变函数声明 | Change Function Declaration | 1 函数名字不够贴切函数所做的事情 2 函数参数增加 3 函数参数减少 4 函数参数概念发生变化 5 函数因为某个参数导致的函数应用范围小(全局有很多类似的函数,在做着类似的事情) | ①对函数内部进行重构(如果有必要的话) ②使用提炼函数手法,将函数体提炼成一个新函数(加后缀:old、new) ③在新函数内做我们的变更(新增参数、删除参数、改变参数释义等) ④改变函数调用的地方(如果是新增、修改、删除参数) ⑤测试 ⑥对旧函数使用内联函数来调用或返回新函数 ⑦如果使用了临时名字,使用改变函数声明将其改回原来的名字(这时候就要删除旧函数了) ⑧测试 | 下一次再看到这段代码时,我就不用再费力搞懂其中到底在干什么。 修改参数列表不仅能增加函数的应用范围,还能改变连接一个模块所需的条件,从而去除不必要的耦合 | |
引入参数对象 | Import Object Parameter | 1 一组参数总在一起出现 2 函数参数过多 | ①创建一个合适的数据结构(对象或数组)——如果已经有了,可以略过 ②使用改变函数声明手法给原函数增加一个参数为我们新的结构 ③测试 ④旧数据中的参数传到新数据结构(变更调用方) 【重复】{ ⑤删除一项旧参数,并将之使用替换为新参数结构 ⑥测试 } | 简化参数 | |
函数组合成类 | Combine Functions into Class | 1 一组函数(行为)总是围绕一组数据做事情 2 客户端有许多基于基础数据计算派生数据的需求 3 一组函数可以自成一个派系,而放在其他地方总是显得不够完美 4 如果在另一个代码库中使用了该变量,这就是一个"已发布变量"(published variable),此时不能进行这个重构。 | ①如果这一组数据还未做封装,则使用引入参数对象手法对其封装 ②运用封装记录手法将数据记录封装成数据类 ③使用搬移函数手法将已有的函数加入类(如果遇到参数为新类的成员,则一并替换为使用新类的成员) ④替换客户端的调用 ⑤将处理数据记录的逻辑运用提炼函数手法提炼出来,并转为不可变的计算数据 | 类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他部分 | |
函数组合成变换 | Combine Functions into Transform | 1 函数组合成变换手法时机等同于组合成类的手法,区别在于其他地方是否需要对源数据做更新操作。 5 在软件中,经常需要把数据"喂"给一个程序,让它再计算出各种派生信息,这些派生数值可能会在几个不同地方用到(重复)。 3 4 如果在另一个代码库中使用了该变量,这就是一个"已发布变量"(published variable),此时不能进行这个重构 | ①声明一个变换函数(工厂函数) ②参数为需要做变换的数据(需要deep clone) 【重复】{ ③计算逻辑移入变换函数内(比较复杂的可以使用提炼函数手法做个过渡) ④测试 } | 有了变换函数,我就始终只需要到变换函数中去检查计算派生数据的逻辑,避免到处重复 | |
拆分循环 | Split Loop | 1 一个循环做了多件不相干事 2 不为别的,就因为这样可以只循环一次。 3 不用担心多循环一次带来的性能问题 4 | ①复制循环 ②如果有副作用则删除单个循环内的重复片段 ③提炼函数 ④优化内部 | 确保每次修改时你只需要理解要修改的那块代码的行为 | |
以管道替代循环 | Replace loop with pipe | 1 一组虽然在做相同事情的循环,但是内部过多的处理逻辑,使其晦涩难懂 2 可以使用array_map、array_filter、collect等函数简化的场景 3不合适的管道(如过滤使用some) | ①创建一个新变量,用来存放每次行为处理后,参与循环的剩余集合 ②选用合适的管道,将每一次循环的行为进行搬移 ③搬移完所有的循环行为,删除整个循环 | 简化代码 | |
将查询函数和修改函数分离 | Separate Query from Modifier | 1 一个函数既有返回值又有设置值 2 某个函数既做查询,又做更新。 | 建立不同的函数,按职责分离代码,注意逻辑顺序 ①复制一份目标函数并改名为查找函数的名字 ②将被复制的函数删除设置值的代码 ③将调用者替换为新函数,并在下面调用原函数 ④删除原函数返回值 ⑤将原函数和新函数中的相同代码进行优化 | 单一职责 | |
移动语句 | AA | 移动语句一般用于整合相关逻辑代码到一处,这是其他部分手法的基础 2 <代码相关逻辑整合一处方便我们对这部分代码优化和重构 | ①确定要移动的语句要移动到哪(调整的目标是什么、该目标能否达到) ②确定要移动的语句是否搬移后会使得代码不能正常工作,如果是,则放弃 | 便于封装 | |
移除死代码 | AA | 1 代码随着迭代已经变得没用了。 2 即使这段代码将来很有可能还会使用,那也应该移除,毕竟现在版本控制很实用。 | ①如果不可以外部引用,则放心删除(如果可能将来极有可能会启用,在这里留下一行注释,标示曾经有过这段代码,以及它被删除的那个提交的版本号) ②如果外部引用了,则需要仔细确认还有没有其他调用点(有eslint规则限制的话。其实可以先删了,看有没有报错) | 检查引用,减少干扰 |
【函数组合成变换】的替代方案是【函数组合成类】,后者的做法是先用源数据创建一个类,再把相关的计算逻辑搬移到类中。
这两个重构手法都很有用,我常会根据代码库中已有的编程风格来选择使用其中哪一个。
不过,两者有一个重要的区别:如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,我就会遭遇数据不一致。