第一组手 法:从看不进门到拆成模块
1. 这一章讲什么:名录怎么读,以及第一条决策线
从这里开始进入原书的重构名录。每条手法在原书里是固定五段:名称、速写、动机、做法、范例;「做法」写的是婴儿学步级别的步骤——「真正工作时,我通常会采用比这里介绍的“婴儿学步”稍大些的步骤」,但一出问题就退回小步1。总口诀一句:「小步前进,情况越复杂,步子就要越小」2。名录不是读的,是查的;本章不逐条罗列,而是抽出作者反复使用的第一条决策线:一段读不进门、改不动的代码,怎么一步步获得结构。
2. 顶层全景
读不懂 ──→ 提炼函数 / 提炼变量(给它名字和边界)
名字碍事 ─→ 内联函数 / 内联变量(把没用的间接拆掉)
关节不对 ─→ 改变函数声明(改名/增删参数)
数据要搬 ─→ 先封装变量(把改数据的难题变成改函数)
结伴出现 ─→ 引入参数对象 / 组合成类 / 组合成变换
两件事 ─→ 拆分阶段(顺序劈开,中转数据结构接头)
图说:自上而下正是第 1 章走查的前半程;提炼与内联互为反向,错了就退回来。
3. 看不懂 → 提炼函数、提炼变量
提炼函数的动机只有一个核心:将意图与实现分开——需要花时间浏览才能弄清一段代码在干什么,就把它提炼成函数,按它「做什么」命名;以后读到名字就够了,不必关心它怎么做3。作者由此接受了一个极端推论:函数「一旦超过6行,就开始散发臭味」,1 行函数照写不误——他举 Smalltalk 时代的例子,highlight 方法的名字比实现 还长,只要名字说清了意图,这不算浪费4。(担心的性能问题如今罕见,短函数反而利于编译器缓存(把常用代码暂存起来加速)。)
提炼变量是同一逻辑的轻量版:复杂表达式难以阅读时,把其中一部分命名成局部变量——它同时是调试时的抓手。两分叉:「如果这个名字只在当前的函数中有意义」,提炼变量就够;名字在更宽的上下文也有意义,就提炼成函数,让别处复用5。在类里做这件事时还有升级形态:底价、折扣这类概念属于整个类,就从变量升级成取值函数,所有客户端共享。
何时提炼会失败:要提炼的代码里被赋值的局部变量太多——「这时最好是先放弃提炼」,先拆分变量或把临时变量查询化,再回来抽6。返回值超过一个也同理:最好的选择是「挑选另一块代码来提炼」,每个函数只返回一个值7。
4. 名字碍事 → 内联
提炼的反向手法同样常用。内联函数:函数体和名字一样清晰时,「间接性可能带来帮助, 但非必要」的间接就该拆掉8;另一种场景是一群函数组织得乱七八糟——先全部内联成一个大函数,再按想要的方式重新提炼。判据:「间接层有其价值,但不是所有间接层都有价值」9。前提检查:函数不能有子类覆写(多态函数内联不得);递归(函数调用自己)、多返回点这类复杂情况「就不应该使用这个重构手法」。
内联变量对称:变量名「并不比表达式本身更具表现力」、或者妨碍旁边的重构时,把它消掉10。
5. 关节不对 → 改变函数声明
「函数是我们将程序拆分成小块的主要方式。函数声明则展现了如何将这些小块组合在一起工作——可以说,它们就是软件系统的关节」11。改名和增删参数共用这一条手法。起名有一个好门道:先写一句注释描述函数用途,「再把这句注释变成函数的名字」12。
参数的取舍没有公式:同一个「判断支付是否逾期」的函数,参数传支付对象还是到期日?传对象耦合了接口但换来封装度;「唯一正确的答案是“没有正确答案”,而且答案还会随着时间变化」——所以才需要一条能反复改关节的手法13。
这条手法有两套做法,是全名录唯一的例外:
| 简单做法 | 迁移式做法 | |
|---|---|---|
| 动作 | 一次改掉声明和所有调用者 | 提炼出新函数→旧函数转发→逐个迁移调用者→内联旧函数 |
| 适用 | 调用者少、有自动化工具 | 调用者多、多态函数、已发布的对外接口 |
主走查(迁移式改参数,数字均为原书示例): 函数 inNewEngland(aCustomer) 判断顾客是否来自新英格兰,实现是查一个州码数组 ["MA", "CT", "ME", "VT", "NH", "RI"],参数是一整个顾客对象。目标:把参数从「顾客」缩小成「州码」,让函数不再依赖「顾客」概念。走法:①先在函数体内把 aCustomer.address.state 提炼成变量 stateCode;②把函数体提炼成临时名新函数 xxNEWinNewEngland(stateCode)(名字要「好记又独特」,回头好替换);③旧函数内联回调用处——现在所有调用者写成 inNewEngland(c.address.state),一次只改一个;④用改变函数声明把临时名改回 inNewEngland。终点:函数签名里只剩一个字符串州码,测试全程保持绿14。如果这是外部客户在调的已发布接口,③④之间可以无限期暂停:把旧函数标记为 deprecated 等客户端迁移,「即便永远也抵达不了“删除旧函数”这个快乐的终点,至少新代码有了一个更好的名字」15。
6. 数据要搬 → 先封装变量
函数好搬,数据难搬——数据没有「转发」机制,搬走就得同步改所有引用。解法是把问题降维:「把“重新组织数据”的困难任务转化为“重新组织函数”这个相对简单的任务」——先用一对读/写函数把数据的所有访问管起来,以后搬数据只需要改这两个函数16。作者的习惯是硬性的:「对于所有可变的数据,只要它的作用域超出单个函数,我就会将其封装起来」17;更进一步的偏好是不可变:「不可变性是强大的代码防腐剂」——不能改的数据不需要验证和钩子,可以放心复制、不必担心谁改了它18。(封装的完整机制——副本、集合、只读代理——第 06 章专讲。)
7. 结伴出现 → 参数对象、组合成类、组合成变换
几项数据总结伴出没于函数签名,是坏味道「数据泥团」(第 03 章);引入参数对象把它们打包。原书范例:检查气象站温度(每条读数是一个摄氏度数值)是否超标的函数 readingsOutsideRange(station, min, max),min/max 这对上下限从运作计划里取了又传、换了名字还各叫各的;收进一个 NumberRange(数值范围)对象后,签名瘦身为 (station, range),47、53、58、53、51 这些读数与范围的比较逻辑有了着落。值得注意的是作者对这项手法的定位:缩短参数列表只是表象,「这项重构真正的意义在于,它会催生代码中更深层次的改变」——一旦新结构有了名字,围绕它的行为就会逐渐聚集过来,长成一个真正的抽象19。
一组函数形影不离地操作同一块数据,该考虑函数组合成类:「如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),我就认为,是时候组建一个类了」——类给这些函数一个共同的家,内部互调不用再传参20。它的孪生(成对出现)手法是函数组合成变换:不 建类,建一个函数,吃进源数据、算出所有派生数据、填进输出记录。怎么选?一条判据:「如果代码中会对源数据做更新,那么使用类要好得多」——变换会把派生数据存在新记录里,源数据一改就是不一致21。
8. 两件事 → 拆分阶段
「每当看见一段代码在同时处理两件不同的事,我就想把它拆分成各自独立的模块」——修改时就能只考虑一个主题22。最典型的样板是编译器:词法分析、解析成语法树、转换优化、生成目标码,每一步边界清楚。识别线索:「如果一块代码中出现了上下几段,各自使用不同的一组数据和函数,这就是最明显的线索」23。做法:把后一段提炼成函数,引入中转数据结构作两阶段的接头,再逐个参数检查——第一阶段造的值搬进中转结构,第一阶段用不到的留在参数上。第 1 章的 createStatementData + 渲染函数正是这条手法的完整走查;原书名录里另给了订单价格的例子(商品相关的前两行、运费的后两行,中转结构叫 priceData)。
9. 作者的判断与证据
| 判断 | 证据 |
|---|---|
| 提炼的判据是「意图与实现分开」,不是代码长度、复用次数 | 作者对三种流行观点的逐一否决3 |
| 函数声明是系统的关节,值得反复调整 | 论证+两套做法的设计;唯一给双做法的手法 |
| 不可变数据优先于封装 | 经验立场,给出理由(免验证、免钩子、可复制)18 |
| 名录只收「常用且值得命名」的手法 | 作者明说不求巨细靡遗;反向手法只收有意思的 |
判断(我们的,不是书里的): 五段式名录真正的产物不是手法本身,而是手法之间的高频组合路径——提炼之前先移动语句、改名之前先封装、拆阶段之前先提炼。单条手法都简单,读得懂组合路径才是「会重构」。原书把这层知识埋在范例的相互引用里。如果错,会错在: 如果读者只背单条手法的步骤,遇到真实代码仍不知道从哪下手;名录的检索(先查再用)式用法(「知道用哪个再查步骤」)本身要求你先会诊断。
10. 边界与局限
- 迁 移式做法在自动化重构工具普及后用武之地变少——工具能安全地一步改完大多数签名(工具能力与语言相关,第 12 章展开)24。
- 「一函数一返回值」是偏好不是铁律;真要返回多个值可以打包记录,但作者首选重新切块。
- 拆分阶段的前后两界不是永远清晰:边界选错就要把字段在中转结构里搬来搬去,成本回头才显现。
11. 可带走的
- 读着费劲的代码块,提炼成函数,以「做什么」命名——名字比实现长没关系。
- 名字只在函数内有意义→提炼变量;更宽上下文也有意义→提炼成函数。
- 间接层有价值才保留:函数体和名字一样清楚就内联掉。
- 改签名先问:调用者都能一次改完吗?不能就走迁移式(临时名→转发→逐个迁移)。
- 对外发布的接口:新函数上线、旧函数标废弃、慢慢迁,删除日期不必承诺。
- 搬数据之前先封装访问函数,把「改数据」降维成「改函数」。
- 数据结伴→参数对象;函数结伴+会改源数据→类;只读派生→变换。
- 一个函数干两件事,先劈阶段再谈别的;中转数据结构是两个阶段的合同。
12. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 名录五段式 / 婴儿学步 | 第5章 §5.1 | text/13-ch05.txt:10(搜「名称(name)」) · text/13-ch05.txt:30(搜「稍大些的步骤」) · text/13-ch05.txt:35(搜「步子就要越小」) |
| 意图与实现分开 | 第6章 §6.1 | text/14-ch06.txt:54(搜「将意图与实现分开」) |
| 6 行臭味 / highlight | 第6章 §6.1 | text/14-ch06.txt:59(搜「超过6行」) · text/14-ch06.txt:63(搜「highlight」) |
| 放弃提炼的时机 | 第6章 §6.1 | text/14-ch06.txt:105(搜「先放弃提炼」) |
| 内联的动机 | 第6章 §6.2 | text/14-ch06.txt:421(搜「间接性可能带来帮助」) · text/14-ch06.txt:428(搜「间接层有其价值」) |
| 提炼变量 | 第6章 §6.3 | text/14-ch06.txt:544(搜「非常复杂而难以阅读」) · text/14-ch06.txt:552(搜「更宽的上下文中也有意义」) |
| 内联变量 | 第6章 §6.4 | text/14-ch06.txt:701(搜「并不比表达式本身更具表现力」) |
| 关节 | 第6章 §6.5 | text/14-ch06.txt:734(搜「软件系统的关节」) |
| 注释变名字 | 第6章 §6.5 | text/14-ch06.txt:744(搜「再把这句注释变成函数的」) |
| 没有正确答案 | 第6章 §6.5 | text/14-ch06.txt:764(搜「没有正确答案」) |
| 迁移式做法 | 第6章 §6.5 | text/14-ch06.txt:793(搜「迁移式做法」) · text/14-ch06.txt:812(搜「不推荐使用」) |
| inNewEngland 走查 |