《重构》7-12章读书笔记
《重构》7-12章读书笔记
重构手法介绍
每个手法通常包含三个模块:时机(遇到什么情况下使用)、做法(详细步骤的概括)、关键字(做法的缩影)
提炼函数
- 时机:
- 当我们觉得一段大函数内某一部分代码在做的事情是同一件事,并且自成体系,不与其他掺杂时
- 当代码展示的意图和真正想做的事情不是同一件时候,如作者提到的例子。想要高亮,代码意思为反色,这样就不容易让人误解,印证了作者前面说的:当你需要写一行注释时候,就适合重构了
- 做法:
- 一个以他要做什么事情来命名的函数
- 待提炼代码复制到这个函数
- 检查这个函数内的代码的作用域、变量
- 编译查看函数内有没有报错(js可以通过eslint协助)
- 替换源函数的被提炼代码替换为函数调用
- 测试
- 替换其他代码中是否有与被提炼的代码相同或相似之处
- 关键字:
新函数、拷贝、检查、作用域/上下文、编译、替换、修改细节
内联函数
- 时机:
- 函数内代码直观表达的意思与函数名字相同
- 有一堆杂乱无章的代码需要重构,可以先内联函数,再通过提炼函数合理重构
- 非多态性函数(函数属于一个类,而这个类被继承)
- 做法:
- 检查多态性(如果该函数属于某个超类,并且它具有多态性,那么就无法内联)
- 找到所有调用点
- 将函数所有调用点替换为函数本体(非一次性替换,可以分批次替换、适应新家、测试)
- 删掉该函数的定义(也可能会不删除,比如我们放弃了有一些函数调用,因为重构为渐进式,非一次性)
- 关键字:
检查多态、找调用并替换、删除定义
提炼变量
- 时机:
- 一段又臭又长的表达式
- 在多处地方使用这个值(可能是当前函数、当前类乃至于更大的如全局作用域)
- 做法:
- 确保要提炼的表达式,对其他地方没有影响
- 声明一个不可修改的变量,并用表达式作为该变量的值
- 用新变量取代原来的表达式
- 测试
- 交替使用3、4
- 关键字:
副作用、不可修改的变量、赋值、替换
内联变量
- 时机:
- 变量没有比当前表达式有什么更好的释义
- 变量妨碍了重构附近代码
- 做法:
- 检查确认变量赋值的右侧表达式不对其他地方造成影响
- 确认是否为只读,如果没有声明只读,则要先让他只读,并测试
- 找到使用变量的地方,直接改为右侧表达式
- 测试
- 交替使用3、4
- 关键字
副作用、只读、替换变量
改变函数声明
最好能把大的修改拆成小的步骤,所以如果你既想修改函数名,又想添加参数最好分成两步来做。
不论何时,如果遇到了麻烦,请撤销修改,并改用迁移式做法)
- 时机:
- 函数名字不够贴切函数所做的事情
- 函数参数增加
- 函数参数减少
- 函数参数概念发生变化
- 函数因为某个参数导致的函数应用范围小(全局有很多类似的函数,在做着类似的事情)
- 做法(适用于确定了函数或者参数只在有限的小范围内使用,并且仅仅改名)
- 先确定函数体内有没有使用这个参数(针对于参数)
- 确定函数调用者(针对于函数)
- 修改函数/参数的声明,使其达到我们想要的效果
- 找到所有的函数/参数声明的地方将其改名
- 找到所有函数/参数调用的地方将其替换
- 关键字
使用变量者、函数调用者、修改函数、声明改名、调用替换
- 做法(标准化做法)
- 对函数内部进行重构(如果有必要的话)
- 使用提炼函数手法,将函数体提炼成一个新函数,同名的话,可以改为一个暂时的易于搜索的随意名字(如:aaa_getData,只要好搜索且唯一即可。),非同名的话,使用我们想要的名字作为新函数名字
- 在新函数内做我们的变更(新增参数、删除参数、改变参数释义等)
- 改变函数调用的地方(如果是新增、修改、删除参数)
- 测试
- 对旧函数使用内联函数来调用或返回新函数
- 如果使用了临时名字,使用改变函数声明将其改回原来的名字(这时候就要删除旧函数了)
- 测试
- 关键字:
内部重构、提炼新函数、好搜索的临时名字、变更、改变调用、旧函数使用新函数、改变调用名字
封装变量
- 时机:
- 当我们在修改或者增加使用可变数据的时候
- 数据被大范围使用(设置值)
- 对象、数组无外部变动需要内部一起改变的需求时候,最好返回一份副本
- 做法:
-
创建封装函数(包含访问和更新函数)
-
- 修改获取这个变量和更新这个变量的地方
-
测试
-
控制变量外部不可见(可以借助es6类中的get来实现不可变量以及限制可见)
-
测试
- 关键字:
新函数、替换调用、不可见
变量改名
- 时机:
- 变量/常量的名字不足以说明字段的意义
- 垃圾命名
- 做法:
- 针对广泛使用的
1.1 先用封装变量手法封装
1.2 找到所有使用该变量的代码,修改测试(如果是对外已发布的变量,可以标记为不建议使用(作者没提到,但是个人感觉是可以这样的)
1.3 测试 - 只作用于某个函数的直接替换即可
- 替换过程中可以以新名字作为过渡。待全部替换完毕再删除旧的名字
- 关键字:
封装变量手法、替换名字、中间过渡
引入参数对象
- 时机:
- 一组参数总在一起出现
- 函数参数过多
- 做法:
- 创建一个合适的数据结构(如果已经有了,可以略过)
数据结构选择:一种是以对象的形式,一种是以类的形式,作者推荐以类的形式,但是在我看来,要根据场景,如果这组数据以及其相关行为可以变为一组方法,如数组类里面的比较两个数组是否完全一致,这就可以以类来声明(js中也可以以export来导出而使用) - 使用改变函数声明手法给原函数增加一个参数为我们新的结构
- 测试
- 旧数据中的参数传到新数据结构(变更调用方)
- 删除一项旧参数,并将之使用替换为新参数结构
- 测试
- 重复5、6
- 关键字:
新结构、增加参数、入参新结构、删除旧参数、使用新结构
函数组合成类
- 时机:
- 一组函数(行为)总是围绕一组数据做事情
- 客户端有许多基于基础数据计算派生数据的需求
- 一组函数可以自成一个派系,而放在其他地方总是显得不够完美
- 做法:
- 如果这一组数据还未做封装,则使用引入参数对象手法对其封装
- 运用封装记录手法将数据记录封装成数据类
- 使用搬移函数手法将已有的函数加入类(如果遇到参数为新类的成员,则一并替换为使用新类的成员)
- 替换客户端的调用
- 将处理数据记录的逻辑运用提炼函数手法提炼出来,并转为不可变的计算数据
- 关键字:
提炼变量、封装成类、移入已有函数、替换调用、移入计算数据
函数组合成变换
- 时机:
- 函数组合成变换手法时机等同于组合成类的手法,区别在于其他地方是否需要对源数据做更新操作。 如果需要更新则使用类,不需要则使用变换,js中推荐类的方式
- 做法:
- 声明一个变换函数(工厂函数)
- 参数为需要做变换的数据(需要deepclone)
- 计算逻辑移入变换函数内(比较复杂的可以使用提炼函数手法做个过渡)
- 测试
- 重复3、4
- 关键字:
变换函数、变换入参、搬移计算逻辑
封装记录
- 时机:
- 可变的记录型结构
- 一条记录上有多少字段不够直观
- 有需要对记录进行控制的需求(个人理解为需要控制权限、需要控制是否只读等情况)
- 需要对结构内字段进行隐藏
- 做法:
- 首先用封装变量手法将记录转化为函数(旧的值的函数)
- 声明一个新的类以及获取他的函数
- 找到记录的使用点,在类内声明设置方法
- 替换设置值的方法(es6 set)
- 声明一个取值方法,并替换所有取值的地方
- 测试
- 删除旧的函数
- 当我们需要改名时,可以保留老的,标记为不建议使用,并声明新的名字进行返回
- 关键字:
转化函数、取值函数、设值函数、替换调用者、替换设置者
以对象取代基本类型
- 时机:
- 随着开发迭代,我们一个简单的值已经不仅仅只是简单的值那么简单了,他可能还要肩负一些其他的职责,如比较、值行为等
- 一些关键的、非仅仅只有打印的功能的值
- 做法:
- 如果没被封装,先使用封装变量手法
- 为要修改的数据值创建一个对象,并为他提供取值、设值函数(看需求)
- 使用者(可能是另外一个大类)修改其取值设值函数
- 测试
- 修改大类中的取值设值函数的名称,使其更好的语义化
- 为这个新类增加其行为(可能是转换函数、比较函数、特殊处理函数、操作函数)等
- 根据实际需求对新类进行行为扩展(如果有必要的话)
- 修改外部客户端的使用
- 关键字:
新类、取设值函数、行为入类、扩展类
以查询取代临时变量
- 时机:
- 修改对象最好是一个类(这也是为什么提倡class,因为类可以开辟一个命名空间,不至于有太多全局变量)
- 有很多函数都在将同一个值作为参数传递
- 分解过长的冗余函数
- 多个函数中重复编写计算逻辑,比如讲一个值进行转换(好几个函数内都需要这个转换函数)
- 如果这个值被多次修改,应该将这些计算代码一并提炼到取值函数
- 做法:
- 检查是否每次计算过程和结果都一致(不一致则放弃)
- 如果能改为只读,就改成只读
- 将变量赋值取值提炼成函数
- 测试
- 去掉临时变量
- 关键字:
只读、提炼函数、删变量
提炼类
- 时机:
- 一个大的类在处理多个不同的事情(这个类不纯洁了)
- 做法:
- 确定分出去的部分要做什么事情
- 创建一个新的类,表示从旧地方分离出来的责任
- 旧类创建时,为新类初始化
- 使用搬移函数手法将需要的方法搬移到新的类(搬移函数时候就将调用地方改名)
- 删除多余的接口函数,并为新类的接口取一个适合自己的名字
- 考虑是否将新的类开放为公共类
- 关键字:
职责边界确认、创建新域、新旧同步初始化、行为搬家、接口删除
内联类
- 时机:
- 一个曾经有很多功能的类,在重构过程中,已经变成一个毫无单独职责的类
- 需要对两个类重新进行职责划分
- 做法:
- 将需要内联的类中的所有对外可调用函数(也可能是字段)在目标类中新建一个对应的中间代理函数
- 修改调用者,调用代理方法并测试
- 将原函数中的相关方法(字段)搬移到新地方并测试
- 原类变为空壳后就可以删除了
- 关键字:
代理、修改调用者、方法搬家、抛弃旧类
隐藏委托关系
- 时机:
- 一个类需要隐藏其背后的类的方法或事件
- 一个客户端调用类的方法时候,必须知道隐藏在后面的委托关系才能调用
- 做法:
- 在服务类(对外的类)中新建一个委托函数,让其调用受托类(背后的类)的相关方法
- 修改所有客户端调用为这个委托函数
- 重复12直到受托类全部被搬移完毕,移除服务类中返回受托类的函数
- 关键字:
委托函数、替换调用者、删除委托整个类
移除中间人
- 时机:
- 因为隐藏委托关系(当初可能是比较适合隐藏的)手法造成的现在转发函数越来越多
- 过度的迪米特法则造成的转发函数越来越多
- 做法:
- 在服务类(对外)内为受托对象(背后的类)创建一个返回整个委托对象的函数
- 客户端的调用转为连续的访问函数进行调用
- 删除原本的中间代理函数
- 关键字:
委托整个类、修改调用、删除代理
替换算法
- 时机:
- 旧算法已经不满足当前功能
- 有更好的方式可以完成与旧算法相同的事情(通常是因为优化)
- 做法:
- 保证待替换的算法为单独的封装,否则先将其封装
- 准备好更好的算法,
- 替换算法过去
- 运行并测试新算法与旧算法对比(一定要对比,也许你选的还不如以前呢)
- 关键字:
算法封装、编写新算法、替换算法、比较算法
搬移函数
- 时机:
- 随着对项目(模块)的认知过程中,也可能是改造过程中,一些函数已经脱离了当前模块的范围
- 一个模块内的一些函数频繁的与其他模块交互,却很少和自身内部进行交互(出现了叛变者)
- 一个函数在发展过程中,现在他已经有了更通用的场景
- 做法:
- 查找要搬移的函数在当前上下文中引用的所有元素(先将依赖最少的元素进行搬离)
- 考虑待搬移函数是否具有多态性(复写了超类的函数或者被子类重写)
- 复制函数到目标上下文,调整函数,适应新的上下文
- 函数内使用的变量考虑是一起搬移还是以参数传递
- 改写原函数为代理函数(也可以内联)
- 检查新函数是否可以继续进行搬离
- 关键字:
确定关系、确定继承、优先基础、函数搬家、相关部分位置确定、原址代理、优化新函数
搬移字段
- 时机:
- 随着业务推进过程中,原有的数据结构已经不能很好的表示程序的逻辑
- 每当调用一个函数时,需要传入的记录参数,总是需要传入另一条记录或者他的某些字段一起
- 修改(行为)一条记录时,总是需要同时改动其他记录
- 更新(数据)一条字段时,总是需要同时在多个结构中作出修改
- 做法:
- 源字段已经被封装(如果未封装,则应该先使用封装变量手法对其封装)
- 目标对象上创建一个字段,及其访问函数
- 源对象对目标对象的字段做对应的代理
- 调整源对象的访问函数,令其使用目标对象的字段
- 测试
- 移除源对象的字段
- 视情况而定决定是否需要内联变量访问函数
- 关键字:
封装、新字段、源址代理、代理新址、旧字段移除、确定是否内联
搬移语句到函数
- 时机:
- 重复代码
- 每次调用a方法时,b操作也总是每次都执行
- 某些语句放在特定函数内更像一个整体
- 做法:
- 将重复代码使用搬移函数手法到紧邻目标函数的位置
- 如果目标函数紧被唯一一个原函数调用,则只需要将原函数的重复片段粘贴到目标函数即可
- 选择一个调用点进行提炼函数,将目标语句函数与语句提炼成一个新的函数
- 修改函数其他调用点,令他们调用新提炼的函数
- 调整函数的引用点
- 内联函数手法将目标函数内联到新函数里
- 移除原目标函数
- 对新函数应用函数改名手法(改变函数声明的简单做法)
- 关键字:
代码靠近、单点提炼、中间函数、修改引用、函数内联、原函数删除、函数改名
函数搬移到调用者
- 时机:
- 随着系统前进过程中,函数某一块的作用发生改变,不再适合原函数位置
- 之前在多个地方表现一致的行为,如今在不同调用点面前表现了不同的行为
tips: 本手法只适合边界有些许偏移的场景,不适合相差较大的场景
- 做法:
- 简单情况下,直接剪切
- 将不想搬移的部分提炼成与当前函数同级函数(如果是超类方法,子类也要一起提炼)
- 原函数调用新的同级函数
- 替换调用点为新的同级函数和要内联的语句
- 删除原函数
- 使用函数改名手法(改变函数声明的简单做法)改回名字
- 关键字:
提炼不变的为临时方法、搬移语句、删除原,改名字
以函数调用替换内联代码
- 时机:
- 函数内做的某些事情与已有函数重复
- 已有函数与函数之间希望同步变更
- 做法:
- 内联代码替换为函数(可能有参数,就要对应传递)
- 关键字:
内联替换
移动语句
- 时机:
- 移动语句一般用于整合相关逻辑代码到一处,这是其他部分手法的基础
- 代码相关逻辑整合一处方便我们对这部分代码优化和重构
- 做法:
- 确定要移动的语句要移动到哪(调整的目标是什么、该目标能否达到)
- 确定要移动的语句是否搬移后会使得代码不能正常工作,如果是,则放弃
- 关键字:
确定副作用、确定目标
拆分循环
- 时机:
- 一个循环做了多件不相干事
- 做法:
- 复制循环
- 如果有副作用则删除单个循环内的重复片段
- 提炼函数
- 优化内部
- 关键字:
复制循环、行为拆分、函数提炼
以管道替代循环
- 时机:
- 一组虽然在做相同事情的循环,但是内部过多的处理逻辑,使其晦涩难懂
- 不合适的管道(如过滤使用some)
- 做法:
- 创建一个新变量,用来存放每次行为处理后,参与循环的剩余集合
- 选用合适的管道,将每一次循环的行为进行搬移
- 搬移完所有的循环行为,删除整个循环
- 关键字:
新变量、合适的管道、删除整个循环
移除死代码
- 时机:
- 代码随着迭代已经变得没用了。
- 即使这段代码将来很有可能还会使用,那也应该移除,毕竟现在版本控制很实用。
- 做法:
- 如果不可以外部引用,则放心删除(如果可能将来极有可能会启用,在这里留下一行注释,标示曾经有过这段代码,以及它被删除的那个提交的版本号) 2、如果外部引用了,则需要仔细确认还有没有其他调用点(有eslint规则限制的话。其实可以先删了,看有没有报错)
- 关键字:
检查引用
拆分变量
- 时机:
- 一个变量被应用到两种/多种的作用下
- 修改输入参数的值
- 做法:
- 在变量第一次赋值的地方,为函数取一个更加有意义的变量名(尽量声明为const)
- 在第二次赋值地方声明该变量
- 以该变量第二次赋值动作为界,修改此前对该变量的所有引用。让他们引用新的变量
- 测试
- 重复上述,直到变量拆分完毕
- 关键字:
新变量、赋值时声明、替换调用
字段改名
- 时机:
- 记录结构中的字段需要改个名字
- 做法:
- 如果结构简单,可以一次性替换
- 如果记录没有封装,最好是先封装记录
- 修改构造时候做兼容判断(老的值与新的值兼容判断:this.a = data.a || data.b)
- 修改内部设取值函数
- 修改记录数据类中的内部调用
- 测试
- 修改外部调用初始化时候的数据
- 删除初始化兼容判断
- 使用函数改名手法(改变函数声明的简单做法),修改调用处的调用方式及内部取设值函数为新字段名
- 关键字:
封装、兼容初始化、内部取设只返回新字段,修改内部调用,测试、删除兼容、内部取设改名、替换外部调用
以查询取代派生变量
- 时机:
- 两个变量相互耦合
- 设置一个变量的同时,将另一个变量与该变量结合,通过计算后给另一个变量设置值
tips:计算的参考变量,是不可变的,计算结果也是不可变的。可以不重构(还是那句话,不可变的数据,我们就没必要理他)
- 做法:
- 确定可以引起变量发生变化的所有点(如果有来自其他模块变量,需要先用拆分变量手法)
- 新建一个计算函数,计算变量值
- 引入断言(assert),确保计算函数的值与该变量结果相同
- 测试
- 修改读取变量的代码,用内联函数手法将计算函数内联进来)
- 用移除死代码手法将旧的更新点的地方清理掉
- 关键字:
来源确定、结果相同、计算函数、清理更新点
将引用对象改为值对象
- 时机:
- 几个对象中共享了一个对象,并且要联动变更的情况下
- 值对象就是每次设置都直接设置这个值,比如:
值对象:a.b=new b(1)
引用对象:a.b.c=1
- 做法:
- 检查重构的目标是否为不可变对象,如果不是的话,则看看是否可以将其改为不可变对象
- 用移除设值函数手法去掉第一个设引用值函数(每次都用设置值的方式复写整个对象)
- 测试
- 重复2、3
- 判断两次相同输入时候,值是否相等
- 关键字:
不可变、替换设置引用值为设置值
将值对象改为引用对象
- 时机:
- 数据副本在多处使用,并且需要一处变化其他地方同步更新
- 做法:
- 创建一个仓库(如果没有的话),仓库要支持:每次访问相同数据都是一个相同的引用对象、支持注册新数据和获取同一个引用数据(js可以在简单场景下简单的使用{})
- 确保仓库的构造函数有办法找到关联对象的正确实例
- 修改调用点,令其从仓库获取关联对象。
- 测试
- 关键字:
共享仓库、单例的引用对象、替换调用点
分解条件表达式
- 时机:
- 条件逻辑内,过长的函数,导致反而难以理解条件逻辑的场景
- 单个条件逻辑处理的函数过大
- 做法:
- 关键字:
提炼分支、提炼条件、优化判断
合并条件表达式
- 时机:
- 无其他副作用的嵌套if
- 无其他副作用的,且返回一致的并列if
- 这些if都是关联的(可以用是否能提炼出一个合适的函数名来作为依据,但也不是绝对,我们可以选择不提炼函数,但是还是建议是相关的if作为一组)
- 做法:
- 确定条件表达式有副作用,先用将查询函数和修改函数分离的手法对其处理 2、如果是嵌套函数一般是用逻辑与合并,如果是并列的if一般是用逻辑或合并,如果两种均有,就要组合使用了(但是我更建议他们应该分离成多个判断) 3、测试
- 重复2、3
- 对合并后的条件表达式进行提炼函数手法(有必要的话)
- 关键字:
分离副作用、合适的逻辑符、提炼条件函数
以卫语句取代嵌套表达式
- 时机:
- 无其他副作用的嵌套if
- 无其他副作用的,且返回一致的并列if
- 这些if都是关联的(可以用是否能提炼出一个合适的函数名来作为依据,但也不是绝对,我们可以选择不提炼函数,但是还是建议是相关的if作为一组)
- 做法:
- 选取最外层需要被替换的条件逻辑,将其替换为卫语句(单独检查条件、并在条件为真时立刻返回的语句,叫做卫语句)
- 测试
- 重复1、2
- 关键字:
从外而内
以多态取代条件表达式
- 时机:
- 多种并列或者嵌套的条件逻辑,让人难以理解
- switch
- 同行为不同类型的判断
- 做法:
- 确定现有的条件类是否具有多态性,如果没有,可以通过将行为封装成类(借助其他手法如函数组合成类等)
- 在调用方使用工厂函数获得行为对象的实例
- 针对不同类型创建子类(相当于在超类在分化)
- 调用方此时应当通过一个工厂返回合适的子类
- 将超类中针对子类类型所做的判断,逐一移入对应子类进行复写(相关子类复写超类的分支函数),超类只留下默认值
注意:这种手法其实是在面向对象开发中很常用的一种方式,但是如果不是
- 在写一个面向对象很明确的项目
- 这个判断过于大
- 可以明确这些子类抽取出来是有意义的(从后期维护角度来说,需要对其增加一些行为)
- 这个子类可以自成体系
不如将其通过一个json或者map来进行指责划分。在js中我觉得更常用的是以策略来代替if
- 关键字:
多态、继承、封装、行为拆分
引入特例
- 时机:
- 数据结构的调用者都在检查某个特殊值,并且这个值每次所做的处理也都相同
- 多处以同样方式应对同一个特殊值
三种情况 第一种原始为类,特例元素没有设置值的操作 第二种原始为类,特例元素有设置值的操作 第三种 原始就是普通的json
-
做法:
-
- 针对于有自己对应行为的类
- 在原类中为特例元素增加一个函数,用以标记这个特例的情况,默认返回一个写死的就行)
- 为特例创建一个class,用以处理特例的正常逻辑和行为,需要把特例对象及其所有行为放到这个类
- 将本次特例的条件使用提炼函数手法抽成一个在类中的字段函数返回true
- 修改所有调用者为第3步的函数
- 修改第一步创建的类。让它返回我们的特例对象
- 特例中的其他字段
- 针对于只读的类
- 将上面做法的创建一个b类改为在类内创建一个函数,返回对象即可。把特例所需信息全部返回在js
- 针对于原始不是类的
- 为特例对象创建一个函数,返回特例对象的深拷贝状态
- 将本次特例的条件使用提炼函数手法抽成一个统一的函数
- 对第一步创建的函数返回值做特殊增强。 将需要的特例的值,逐一放进来。
- 替换调用者使用函数的返回值
- 关键字:
特例逻辑搬到class、过渡函数、替换调用者、修改新class
将查询函数和修改函数分离
- 时机:
- 一个函数既有返回值又有设置值
- 做法:
- 复制一份目标函数并改名为查找函数的名字
- 将被复制的函数删除设置值的代码
- 将调用者替换为新函数,并在下面调用原函数
- 删除原函数返回值
- 将原函数和新函数中的相同代码进行优化
- 关键字:
新函数为查找、删除设置值、替换调用者、删除返回值、优化
函数参数化
- 时机:
- 有多余一个函数的逻辑非常相似,只是有一些字面量不同(有时候可能会碰到a、b很相似,a、c也很相似,但是b、c差距比较大时候,这种情况个人观点为:将ab、ac中逻辑紧密的抽成一个,不要形式化的就要吧abc抽到一起。反而适得其反)
- 做法:
- 从这一组相似函数中,找到一组,通常来说尽可能选择调用比较少的地方
- 运用改变函数声明手法(改变参数)使其在调用时候,将变化的部分以参数形式传入)
- 修改当前这个函数的所有调用点,为调用新函数,并传递参数
- 修改新函数,让它使用新传进来的参数
- 将其他相似的函数,逐一替换为这个新函数,每次替换都要测试一下
- 关键字:
调用较少、变化点入参、修改调用、替换使用
移除标记参数
- 时机:
- 一个用来控制函数流程的参数
- 做法:
- 针对参数的每一种可能值,新建一个明确函数(如果参数控制整个流程,则可以用分解条件表达式手法创建明确函数,如果只控制一部分函数则创建转发函数,将这些函数,统一通过这些明确函数进行转发)
- 替换调用者
tips:如果是这个标记即作为标记,又作为参数值。则对其进行拆分。
- 关键字:
流程、行为拆分
保证对象完整的手法
- 时机:
- 从一个代码中导出几个值
- 调用者将自身的部分参数传递
- 一般发生在引入参数对象手法之后
- 做法:
- 新建一个空函数(可能是新建,也可能是用提炼函数),接受完整对象
- 新函数体内调用旧函数,并且使用合适的参数列表
- 修改旧函数的调用者,令他使用新函数,修改旧函数内部
- 使用内联函数手法将旧函数内代码搬移到新建的函数 5、修改新函数的名字为旧函数
- 关键字:
接受完整对象、新调用老、修改调用、内联、改名
以查询取代参数
- 时机:
- 一个函数传入了多个相同的值(如总是能根据b参数不需要很复杂就可以查到a参数)
- 调用函数传入了一个函数本身就可以很容易获得的参数(指的是内部或者计算获得,而非从其他模块拿)
- 如果目标函数本身就具有引用透明性(函数的返回值只依赖于其输入值),用查询后,他去访问了一个全局变量,则不适合用本重构
一言以概之:这个函数自身或者通过参数都能得到另一个值就可以使用这个手法
- 做法:
- 如果有必要,可以将参数计算的过程提炼为一个只读变量或者一个函数
- 将函数体内引用该参数的地方,都改为运用计算函数
- 去掉该参数(调用者也要去掉)
- 关键字:
提炼变量、参数消除
以参数取代查询
- 时机:
- 一个函数内部因为引用了全局变量而导致了不透明
- 一个函数内部引用了一个即将被删除的元素
- 一个函数内部,过多的依赖了另一个模块(这种有两种做法:一种是本手法,另一种是搬移函数手法,要根据函数实际作用操作
- 做法:
- 使用提炼变量手法将目标(希望作为参数传入的查询)提炼出来
- 把整个函数体提炼,并且单独放到一个函数内(需要保留计算逻辑,计算逻辑作为代理函数每次的值以参数传入函数)
- 消除刚才提炼出来的变量(旧函数应该只剩下一个简单的调用)
- 修改调用方,改为调用新函数,并传入调用时候计算的计算值
- 删除原函数内的计算代理
- 新函数改回旧函数的名字(如果意义发生变化,需要重新起名字)
- 关键字:
变量提炼、函数体换新、旧函数传参、旧函数调新函数,删除代理函数、函数改名
移除设值函数
- 时机:
- 类内某个字段有一些设值函数
- 类无任何副作用(如:操作渲染html的append、往localstorage写东西、init调用接口、多处共享引用等)
- 很庞大的类(需要先作拆分优化)
- 做法:
- 如果无法拿到设置变化的值,就通过构造函数的参数传入
- 在构造函数内部调用设值函数进行更新
- 移除所有的设置值的函数调用,改为new一个类
- 使用内联函数手法消除设值函数。
tips:可以批量操作多个设值函数。
- 关键字:
设值替换为new
以工厂函数取代构造函数
- 时机:
- 构造函数每次都需要new关键字,又臭又长(个人观点是这条没必要,除非完全忍受不了)
- 构造函数如果不是default导出的话,这个名字那就是固定的。有时候语义化不明显
- 有时虽然都是调用同一个类。但所处环境不同,我调用意义就不同
- 做法:
- 新建一个工厂
- 工厂调用并返回现有的构造函数
- 替换调用者
- 尽可能缩小构造函数可见范围(js中很难实现,可能只能藏的深一些)
- 关键字:
工厂函数、调用类、替换调用
以命令取代函数手法
- 时机:
- 在js中,体现为又臭又长的还没法进行指责划分的函数(可能是它们都属于同一部分逻辑,也可能是因为内部写法导致不好划分)
- 做法:
- 新建一个空的类
- 用搬移函数手法将函数搬移到这个新的类
- 给类改个有意义的名字,如果没什么好名字就给命令对象的实际具体执行的函数起一个通用的名字,如:execute或者call
- 将原函数作为转发函数,去构造类
- 将函数内的参数,改为构造时候传入
- 如果可以将其他字段修改为只读
- 关键字:
新的类、函数搬家、原类转发函数、构造入参、只读
函数上移手法
- 时机:
- 子类中有绝大部分都在复制某个函数
- 这些函数函数体都相同或者近似
- 做法:
- 确保待提升函数的行为完全一致,否则需要先将他们一致化
- 检查函数体内的所有调用和字段都能从超类中调用(如果有不一致则考虑先把它们提升)
- 检查函数名字全部一致,不一致的话先将他们名字统一
- 将函数复制到超类中
- 逐一移除子类中的函数。每一次都要测试
- 关键字:
函数体一致化、名字一致化、引用调用先行、提升函数、删除重写
字段上移手法
- 时机:
- 子类中有绝大部分都在复制某个字段
- 做法:
- 检查该字段的所有使用点,确保是在同样的方式被使用
- 如果名字不同,先把名字统一化
- 移动到父类,并确保子类都能访问父类的这个字段
- 逐一移除子类的该字段
- 关键字:
同样方式使用、统一名字、字段上移、删除子类字段
构造函数本体上移
- 时机:
- 子类中有绝大部分都在复制某个构造函数函数
- 这些构造函数函数体都相同或者近似
- 做法:
- 如果超类没有构造函数,就先定义一个,所有子类增加super关键字
- 使用移动语句将子类的公共语句移动到super紧挨着之后
- 提升到超类构造函数中
- 逐一移除子类的公共代码,如果这个值来自于调用者,则从super上传给父类
- 如果要上移的语句有基于子类的字段而设置初始化的值的,查看是否可以将这个字段上移,如果不能,则使用提炼函数语句,将这句提炼为一个函数,在构造函数内调用他
- 函数上移
- 关键字:
构造函数内的语句上移
函数下移、字段下移
- 时机:
- 超类中的函数(字段)只与一部分子类有关(这个范围需要掌控好,我通常选择如果使用超过三分之二的,并且在剩余的三分之一里面,这个函数/字段没有副作用,就选择上移,否则下移)
- 做法:
- 将超类中的函数(字段)本体逐一复制到每一个需要此函数(字段)的子类中
- 删除超类中的函数(字段)
- 关键字:
按需放置
以子类取代状态码
- 时机:
- 一个类中有一些有必要的多态性被隐藏
- 根据某个状态码来返回不同的行为
- 做法:
- 直接继承超类的
- 将类型码字段进行封装,改为一个get type()的形式
- 选择其中一个类型码,为其创建一个自己类型的子类
- 创建一个选择器逻辑(根据类型,选择正确的子类)把类型码复制到新的子类
- 测试
- 逐一创建、添加选择逻辑的代码
- 移除构造函数的这个参数
- 将与类型相关的代码重构优化
- 间接继承(通过类型的超类而非现有超类进行继承)
-
用类型类包装类型码(以对象取代基本类型手法)
-
走直接继承超类的逻辑,唯一不同的是,这次要继承类型超类,而非当前超类
-
关键字:
封装类型码、多态化、选择子类的函数、移除类型参数
移除子类
- 时机:
- 随着程序发展子类原有行为被搬离殆尽
- 原本是为了适应未来,而增加子类,但是现在放弃了这部分代码。
- 子类的用处太少,不值得保留
- 做法:
- 检查子类的使用者,是否根据不同子类进行处理
- 如果处理了则将处理函数封装为一个函数,并将他们搬移到父级
- 新建一个字段在超类,用以代表子类的类型
- 将选择哪个类来实例化的构造函数搬移到超类
- 逐步搬移所有的类型
- 将原本的类型处理改为使用新建的字段进行判断处理
- 删除子类
- 关键字:
工厂函数取代子、类型提炼、检查类型判断
提炼超类
- 时机:
- 两个类在做类似的事情
- 两个类随着程序发展,有一些共同部分需要合并到一起
- 做法:
- 新建超类(可能已经存在)
- 调整构造函数(从数据开始)
- 调整子类需要的字段
- 将多个子类内共同的行为复制到超类
- 检查客户端代码。考虑是否调整为超类
- 关键字:
相同事情搬移到超类
以委托取代子类
- 时机:
- 类只能继承一个,无法多继承
- 继承给类引入了紧密的关系(超类、子类耦合严重)
- 做法:
- 使用以工厂函数取代构造函数将子类封装
- 创建一个委托类、接受所有子类的数据,如果用到了超类,则以一个参数指代超类
- 超类中增加一个安放委托类的字段
- 增加一个创建子类的工厂,让他初始化超类中的委托字段
- 将子类中的函数搬移至委托类,不要删除委托代码(如果用到了其他元素也要一并搬离)
- 如果这个函数被子类之外使用了,把留在子类的委托移动到超类中,并加上卫语句,检查委托对象初始化
- 如果没有其他调用者,使用移除死代码手法去掉没人使用的委托代码
- 测试
- 重复567。直到所有函数都搬到了委托类
- 找到调用子类的地方,将其改为使用超类的构造函数
- 去掉子类
- 关键字:
工厂函数初始化类、委托类、所有子类数据搬移至委托类、超类增加委托类的字段、子类函数搬移到委托类、删除子类
以委托取代超类手法
- 时机:
- 错误的继承(如父子不是同一个意义的东西,但是子还想要用超类的一些字段)
- 超类不是所有方法都适用于子类
- 做法:
- 在子类中创建一个属性,指向新建的超类实例
- 子类中用到超类的函数,为他们创建转发函数(用上面的属性)
- 去除子类与超类的继承关系
- 关键字:
子类属性指向超类、转发函数、去除继承