6.1 提炼函数(Extract Function)
提炼的关键在于命名
1 | function printOwing(invoice) { |
何时该提炼
常见观点:
- 代码长度:一个函数应该能在一屏中显示
- 代码复用:只要被用过不止一次的代码,就应该单独放进一个函数;只用过一次的代码则保持内联(inline)
- 代码阅读:将意图与实现分开,需要理解、注释的代码就应该提炼,让人一眼过去知道这段代码的意图
大胆用小函数,只要它能表明意图,即使函数名比代码还长。优化就交给编译器吧,代码是给人阅读的。
怎么去提炼
- 根据函数是“做什么”创建合适命名的函数
- 可以先局部嵌套函数来避免变量作用域的问题,然后再搬移函数出去
- 迁移代码,并检查变量作用域
- 仅提炼函数使用 => 提炼变量
- 提炼函数只读 => 传参
- 提炼函数和外部都会修改 => 提炼函数返回新值,如果返回多个,先看能否再拆,再考虑返回结构体之类
- 充分自测,并检查其他代码是否和提炼代码有相同的,避免漏修改
6.2 内联函数(Inline Function)
1 | function rating(aDriver) { |
何时去内联
- 某些函数本身意图就很清晰,重构价值不大,间接性可能带来帮助,但非必要的间接性总是让人不舒服。
- 有一堆不合理的小函数,可以先内联大函数,再拆分
- 太多间接层,A-B-C,再每个类都实现了一层跳转。间接层有其价值,但不是所有间接层都有价值
怎么去内联
- 避免多态重载的函数
- 找到所有调用点,可以逐步替换
- 当引用次数为0时,去掉函数定义
注意的点:
- 不要无脑CV,有时候可能函数内的变量名和外部代码变量名不一致,尽管是同一个引用
- 多依赖IDE的变量是否【存在/使用】提示
6.3 提炼变量(Extract Variable)
1 | return ( |
何时去提炼
- 表达式太长,难以阅读
- 某些变量获取有性能成本,多次使用可以先提取出来
- 如果提取的表达式上下文比较宽,比如在类内也有意义,可以考虑提炼成成员函数(比如范例中的获取单价可以是订单类的一个成员函数)
怎么去提炼
- 注意表达式如果出现多次,需要逐一替换
6.4 内联变量(Inline Variable)
1 | let basePrice = anOrder.basePrice; |
如果表达式意图已经很清晰了,就没必要徒增成本。内联时注意所有调用处,同理多依赖IDE分析。
6.5 改变函数声明(Change Function Declaration)
1 | // 只改名 |
何时去改变声明
- 函数名不合适,有可能上下文变了,也有可能就是一开始没想到合适的命名
- 函数参数不合适,有可能新增新的参数,也有可能原先的参数局限性太高
总之,没人能确保一开始的声明就是对的,随着需求以及我们越来越聪明的脑袋(x,正如著名人士说过,真相不止一个,当时机到了,我们就会希望去修改函数声明。
怎么去改变声明
- 一步到位式修改,改函数声明-改所有调用处-自信提交,适合强类型语言
- 迁移式做法,新增一个新函数,让旧函数去调用新函数,逐步替换已有调用处,最后删除旧函数
- 弱类型语言调用处过多时
- 发布外部的sdk,管不了调用的地方,需要耐心等待其他用户改完(标记旧函数为废弃)
6.6 封装变量(Encapsulate Variable)
1 | let defaultOwnerData = { firstName: "Martin", lastName: "Fowler" }; |
何时去封装
原则上,使用范围越广的变量,越需要封装
把数据搬走,就必须同时修改所有引用该数据的代码,否则程序就不能运行。如果数据的可访问范围很小,比如一个小函数内部的临时变量,那还不成问题。但如果可访问范围变大,重构的难度就会随之增大,这也是说全局数据是大麻烦的原因。
所以将迁移变量转化成迁移函数,会使得迁移成本变低,即封装变量。封装变量还有个好处是可以清晰知道变量被使用和修改的时机。
封装数据很重要,不过,不可变数据更重要,可修改的数据会加大封装成本
怎么去封装
创建变量的取值和设值函数->逐一修改使用变量的地方->设置私有(像lua没法设置,可以对命名进行约束)
注意点:
- 如果返回的是引用,怎么避免外部对引用内容进行修改(比如返回lua中的table)
- 一种是只返回副本,避免外部修改实际的值。(小心有需要修改原值的需求
- 另一种是对变量再封装,避免能直接修改变量的成员(但如果是lua table中层层嵌套,恐怕成本也挺高)
设值函数返回一份副本的好处:可以防止因为源数据发生变化而造成的意外事故。很多时候可能没必要复制一份数据,不过多一次复制对性能的影响通常也都可以忽略不计。但是,如果不做复制,风险则是未来可能会陷入漫长而困难的调试排错过程。
数据被使用得越广,就越是值得花精力给它一个体面的封装
6.7 变量改名(Rename Variable)
使用范围越广,名字的好坏就越重要
- 如果作用域很短,比如匿名函数,上下文已经很清晰,就不需要太讲究命名
- 对于作用域超出一次函数调用的字段,则需要更用心命名
怎么改:
- 局部函数的变量,直接改直接用
- 超过一次函数调用,且多处有取值和修改,请考虑封装变量,再进行改名
- 如果是给常量等不可变数据改名,完全可以新增一个变量,让旧的常量去获取新常量的值,慢慢迁移已有调用处
6.8 引入参数对象(Introduce Parameter Object)
1 | function readingsOutsideRange(station, min, max) { |
一组数据项总是结伴同行,出没于一个又一个函数。这样一组数据就是所谓的数据泥团,可代之以一个数据结构
好处:
- 数据项之间的关系变得明晰
- 参数的参数列表也能缩短
- 把围绕这些数据的行为和数据封装一起成类,使其更内聚
- 比如范例中判断范围的方法本身围绕最大值和最小值,后续还能引入多态进行多种类型的最值判断
6.9 函数组合成类(Combine Functions into Class)
1 | function base(aReading) {...} |
何时去组合
- 一组函数形影不离地操作同一块数据(可能是将数据传入函数)
- 类的力量是强大的,当开始组织类的时候,可能会发现其他的计算逻辑,将它们也重构到新的类当中,慢慢的原本分散的代码会清晰起来
- 类的好处:客户端可以修改对象的核心数据,通过计算得出的派生数据则会自动与核心数据保持一致
但也不能随意将数据和类组织在一起,比如项目中的一些碰撞类,理想情况下他应该是工具类,根据通用的数据去计算碰撞结果。但目前和技能数据一起组合成类,当其他地方也需要检查碰撞时或对碰撞计算新增额外参数时,会无从下手。
可以尝试:
- 将碰撞通用计算的部分抽离,比如传入两个矩形计算是否碰撞等
- 将数据和部分非通用计算放在类似【技能结果类】之类和技能强相关,里面再调用通用的碰撞计算类
- 但如果原先也没有多处传递数据到计算函数的代码,且数据和计算函数的使用足够集中的话,可以适当放任,并不需要组合成类
关键点还是【一组】函数,操控同一块【数据】,且数据和函数强相关,毋庸置疑,我们该借助【类】的力量了。
6.10 函数组合成变换(Combine Functions into Transform)
1 | function base(aReading) {...} |
解决什么问题:
- 需要通过某个数据结构(通常是类)里面的数据去计算新的中间数据,再拿这个中间数据在其他地方使用
- 如果这个中间数据,很多地方都需要使用,散落在代码的多个角落都有计算中间数据的重复逻辑
- 比如角色身上有多个词条属性会影响攻击的伤害加成,存在多个地方计算伤害时,这个伤害加成的计算也会分布到各处
怎么解决:
- 本节是用一个变换函数,将源数据->新数据,新数据带有中间数据,使用中间数据时从新数据的新变量去获取就行
- 另一种方法是把这个中间计算的过程,直接放到类里面
- 还有最基础的做法:将重复计算的过程抽离成单独的函数(不放类里)
核心都是避免重复代码
- 但是变换函数会导致返回的数据是新的数据,如果对数据进行修改时,会造成不能修改到源数据的困惑。而且每次使用变换函数会拷贝一个新实例,对于内存比较敏感的应用还是值得斟酌
- 而只是抽离函数还是会造成对数据的操作不够集中
- 所以还是建议放在类里面作为成员函数来计算
6.11 拆分阶段(Split Phase)
1 | const orderData = orderString.split(/\s+/); |
解决什么问题:
- 一段代码里同时处理了多件事,让阅读者需要反复切换上下文,找不到头脑(写时一时爽,维护看不懂)
- 后来者修改时需要谨慎避免破坏他们"紧密巧妙"的关系
- 往往长代码就是处理了过多的事务