为重构铺路
如果想重构必须编写测试
-
使用自动化测试
-
项目或者需求开始之前开始写测试用例
-
测试驱动开发:编写测试-编程-重构
编写测试的最佳实践
- 当业务逻辑的代码比较复杂,可以把用户逻辑代码和 UI 的测试分开
- 测试的框架:Mocha,支持使用不同的库,支持编写不同类型的断言
- 总是确保测试不该通过时会真的失败(专门写一个一定会出错的例子)
- 频繁的运行测试,当前正在处理的代码,最好几分钟运行一次,每天至少运行一次所有的测试
- 测试的重点应该是那些最担心出错的部分
- 测试代码之间不可产生交互(如提取公共变量),可以将公共部分的代码提取到框架自带的在每个测试单元执行之前都会执行的代码段中(例如:Mocha 的 beforEach)
- 考虑可能出错的边界条件,把测试火力集中在那儿。例如值为空的情况
重构名录
第一组重构
提炼函数:将实现(怎么做)与意图(做什么)分开,以这个函数的意图来命名;如果你需要花时间浏览一段代码才能弄清它到底在干什 么,那么就应该将其提炼到一个函数中
内联函数:函数内部代码和函数名一样清晰易读时可将逻辑内联到调用点
提炼变量: 表达式太长通常是一个信号,拆解表达式,提取变量
改变函数声明:包含两个部分:函数改名,调整参数;调整参数:让函数的逻辑简单些。函数改名:函数声明不能很好的表达函数的意图。
推荐做法:迁移式重构(创建新函数,旧函数调用新函数,渐进式修改所有调用方)
封装变量:数据使用的越广,就越值得花精力给它一个好的封装;不可变变量是很好的代码防腐剂;可以在获取变量的时候返回副本来保证变量的不可变性(特别注意引用类型的变量)
函数组合成类:如果发现一组函数形影不离地操作同一块数据——存在数据泥团;对数据进行拆分组合形成对象或者类(函数的参数列表不可过长)
拆分阶段:将一大段函数行为分成顺序执行的两个或几个阶段(函数,按顺序和传参的方式调用)
封装
永远不要去修改可变数据,数据的处理方式:封装成只读代理和数据副本
我宁愿多复制一份数据,也不愿去调试因意外修改集合招致的错误
以对象取代基本类型:开发初期可能以简单的数据项表示简单的情况,但随着开发的进行,可能会发现会给这个简单的数据添加很多特殊行为——一旦发现对某个数据的操作不仅仅局限于查找时就需要创建一个新类,之后添加的业务逻辑就可以往类里面添加
以查询取代临时变量:封装临时变量的修改逻辑,简化函数
提炼类:当类变得越来越庞大的时候,可以使用提炼类,一个字段一个函数的搬移
内联类:往往作为提炼类的一个中间操作,先内联类,再使用提炼类的手法提炼分离职责
隐藏委托关系:一个好的模块化设计,“封装”即使不是最关键特征也是最关键特征之一。封装意味着每个模块应该尽可能少了解系统的其他部分,对调用者隐藏代码逻辑
移除中间人:隐藏委托关系的反重构,防止一个类只做了转发的事情,何时使用隐藏委托关系,移除中间人,根据实际代码场景来判断,需要思考当前适合什么样的重构手法
替换算法:逻辑实现方式的优化
搬移特性
搬移函数:频繁引用其他上下文中的元素,而对自身上下文中的元素却关心很少的时候就需要搬移函数了(需要让更亲密的元素相会)
即使经验再丰富,技能再熟练,我仍然发现在进行初版设计时往往还是会犯错,在不断编程的过程中,对问题域的理解会加深,对什么是理想的数据结构会有更多想法(表示了重构的重要性)
搬移字段:需要确保字段在前后两个地方的赋值取值一致
搬移语句到函数(搬移语句到调用处):判断语句和函数的整体性
以函数调用取代内联代码:代码封装
移动语句:将关联的东西一起出现,变量就近原则(第一次需要使用变量的地方声明),常常作为其他重构手法的提前步骤,比如提炼函数,对于没有副作用的代码,可以随心所欲的编排它们的顺序,所以尽量去写无副作用的代码
拆分循环:一个循环只做一件事,循环拆分,移动语句,提炼函数,一整套功法
以管道取代循环:使用语言的管道函数取代自己写的循环
移除死代码:怎么也不会执行的函数,逻辑快和怎么也没使用到的变量
重新组织数据
用于重构中对于数据结构的组织
拆分变量
字段改名:分两步走,赋值部分和取值部分
以查询取代派生变量
简化条件逻辑
分解条件表达式:封装判断条件,判断为真的表达式和判断为假的表达式
合并条件表达式:合并最终行为一致的条件判断
以卫语句取代嵌套条件表达式:以单独检查语句取代嵌套检查语句(多个 if else 嵌套)
如果某个条件极其罕见,就应该单独检查该语句,这样的单独检查常常被称为“卫语句”,卫语句表达的是,这种情况不是本函数的核心逻辑所关心的,但如果它真的发生了,请做一些必要的整理工作
以多态取代条件表达式:当分支条件下的语句逻辑比较复杂时,可以使用多态取代 switch case
重构 API 好的 API 会把更新数据的函数与只是读取数据的函数清晰分开
将查询函数和修改函数分离:任何有返回值的函数都不应该有看得到的副作用,命令与查询分离(Command-Query Separation)
函数参数化:相似逻辑的函数,如果可以通过添加参数的方式整理为一个函数则使用这种方法
标记参数:标记参数是这样一种参数,调用者用它来指示被调函数应该执行哪一部分逻辑。如果调用者传入的是程序中流动的数据,这样的参数不算标记参数,只有调用者直接传入字面量值,这才是标记参数。应该针对参数的每一种可能值,新建一个明确函数
保持对象完整:“传递整个记录”的方式能更好的应对变化
以参数取代查询:调用方和函数对变量的依赖关系的责任分配
移除设值函数:清晰表达不希望一个变量的值被修改的意图
以工厂函数取代构造函数:在工厂函数内部可以调用构造函数或者其他方式来创建实例
以命令取代函数:将函数封装到一个对象或者类中,这个对象或者类只有一个职能——执行函数
函数,不管是独立函数,还是以方法形式附着在对象上的函数,是程序设计的基本构造块,不过,将函数封装成自己的对象,有时也是一种有用的办法,这种对象我称之为“命令对象”或者简称“命令”,与普通函数相比,命令提供了更大的控制灵活性和更强的表达能力,除了函数调用还可以支持附加的其他操作,例如撤销,或者通过添加不同的参数来支持丰富的生命周期管理能力(这种将以命令取代函数的模式也叫做命令模式)
以函数取代命令:命令对象为处理复杂计算提供了强大的机制,借助命令对象,可以将复杂的函数拆解为多个方法,彼此之间通过字段共享状态,拆解后的方法可以分别调用(类中有多个方法,但对调用方来说没有感知,有利于后期提取公共函数)。开始之前的数据状态也可以逐步构建,所以当执行逻辑不太复杂,则不需要使用命令对象的形式
处理继承关系
函数上移:子类中的函数移动到超类中,如果不同子类中有逻辑大致相同的函数可以提升到超类中(如果函数体并不完全一致,那就先对它们进行重构,直到函数体完全一致)
字段上移:公共字段上移,注意如果是只有子类才能访问的变量需要定义为 protected 类型
构造函数本体上移:将子类构造函数中的相似的逻辑上移到超类,通过 super(参数) 执行超类中的构造函数
函数下移:超类中的某个函数只与一个或者几个子类有关
字段下移:超类中的某个字段只被一个子类用到,就将其搬移到需要该字段的子类中
以子类取代类型码:软件系统经常需要表现“相似但又不同的东西”,比如员工可以按职位分类,订单可以按优先级分类。 表现分类关系的第一种工具是类型码字段,根据具体的编程语言,可能实现为枚举,符号,字符串或者数字。大多数时候,使用类型码就够了,但往前进一步引入子类,即可以使用多态来处理条件逻辑,并对不同类型的类的特有的逻辑进行封装,可能会使用上面四种重构手法,但需要考虑继承关系,超类子类的定义。可用工厂函数和构造函数的形式
移除子类:有时候添加子类是为了应对未来的功能,如果构想中的功能压根没被构造出来,或者用了另一种方式构造,使该子类不再被需要(子类存在就有成本,阅读者需要花心思去理解它的用意),最好的选择就是移除子类,替换为超类中的一个字段;在替换之前需要检查是否有使用到这个子类的地方
提炼超类:将不同类之间相似的逻辑提炼到超类中
折叠继承体系:超类和子类相差不大时,合并超类和子类
以委托取代子类:导致行为不同的原因可能有很多种,但继承只能处理一个方向上的变化;继承使类之间关系变得非常紧密,在超类上做任何的修改,都很可能破坏子类;使用委托可以解决这两个问题,用《设计模式》的语言去说,就是用状态模式或者是策略模式取代子类 实现方式比较难理解,可以看例子
以委托取代超类:错误的继承关系,需要使用转发来获取其他类的属性
总结
- 消除重复
- 杜绝硬编码
- 命名语义化
- 维持数据不可变性
- 声明的地方尽量靠近使用的地方
- 逻辑清晰,简练,易读(代码是给人看的)
- 分清边界情况与兼容逻辑,不需要为设想的可能性增加了代码的复杂度
重构和开发常常是同时进行的,需要在开发中不断提升代码的鲁棒性,易读性,可扩展性