跳到主要内容

第一组手法:从看不进门到拆成模块

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. 可带走的

  1. 读着费劲的代码块,提炼成函数,以「做什么」命名——名字比实现长没关系。
  2. 名字只在函数内有意义→提炼变量;更宽上下文也有意义→提炼成函数。
  3. 间接层有价值才保留:函数体和名字一样清楚就内联掉。
  4. 改签名先问:调用者都能一次改完吗?不能就走迁移式(临时名→转发→逐个迁移)。
  5. 对外发布的接口:新函数上线、旧函数标废弃、慢慢迁,删除日期不必承诺。
  6. 搬数据之前先封装访问函数,把「改数据」降维成「改函数」。
  7. 数据结伴→参数对象;函数结伴+会改源数据→类;只读派生→变换。
  8. 一个函数干两件事,先劈阶段再谈别的;中转数据结构是两个阶段的合同。

12. 原文地图

主题原书章原文位置
名录五段式 / 婴儿学步第5章 §5.1text/13-ch05.txt:10(搜「名称(name)」) · text/13-ch05.txt:30(搜「稍大些的步骤」) · text/13-ch05.txt:35(搜「步子就要越小」)
意图与实现分开第6章 §6.1text/14-ch06.txt:54(搜「将意图与实现分开」)
6 行臭味 / highlight第6章 §6.1text/14-ch06.txt:59(搜「超过6行」) · text/14-ch06.txt:63(搜「highlight」)
放弃提炼的时机第6章 §6.1text/14-ch06.txt:105(搜「先放弃提炼」)
内联的动机第6章 §6.2text/14-ch06.txt:421(搜「间接性可能带来帮助」) · text/14-ch06.txt:428(搜「间接层有其价值」)
提炼变量第6章 §6.3text/14-ch06.txt:544(搜「非常复杂而难以阅读」) · text/14-ch06.txt:552(搜「更宽的上下文中也有意义」)
内联变量第6章 §6.4text/14-ch06.txt:701(搜「并不比表达式本身更具表现力」)
关节第6章 §6.5text/14-ch06.txt:734(搜「软件系统的关节」)
注释变名字第6章 §6.5text/14-ch06.txt:744(搜「再把这句注释变成函数的」)
没有正确答案第6章 §6.5text/14-ch06.txt:764(搜「没有正确答案」)
迁移式做法第6章 §6.5text/14-ch06.txt:793(搜「迁移式做法」) · text/14-ch06.txt:812(搜「不推荐使用」)
inNewEngland 走查第6章 §6.5text/14-ch06.txt:940(搜「inNewEngland」) · text/14-ch06.txt:967(搜「xxNEWinNewEngland」)
快乐的终点第6章 §6.5text/14-ch06.txt:871(搜「快乐的终点」)
数据变函数第6章 §6.6text/14-ch06.txt:1030(搜「重新组织函数」) · text/14-ch06.txt:1034(搜「作用域超出单个函数」) · text/14-ch06.txt:1047(搜「代码防腐剂」)
参数对象的深意第6章 §6.8text/14-ch06.txt:1288(搜「更深层次的改变」)
组合成类第6章 §6.9text/14-ch06.txt:1456(搜「形影不离地操作同一块数据」)
变换 vs 类第6章 §6.10text/14-ch06.txt:1651(搜「对源数据做更新」)
拆分阶段第6章 §6.11text/14-ch06.txt:1850(搜「两件不同的事」) · text/14-ch06.txt:1866(搜「最明显的线索」)

Footnotes

  1. 出处:「介绍重构名录」第 29 段(text/13-ch05.txt:29,搜「非常小的步骤」)与第 30 段(text/13-ch05.txt:30,搜「稍大些的步骤」)。

  2. 出处:「介绍重构名录」第 35 段(text/13-ch05.txt:35,搜「步子就要越小」)。

  3. 出处:「第一组重构」第 54 段(text/14-ch06.txt:54,搜「将意图与实现分开」)。三种流行观点(一屏、复用)在第 51-53 段(text/14-ch06.txt:52,搜「一屏中显示」)。 2

  4. 出处:「第一组重构」第 59 段(text/14-ch06.txt:59,搜「超过6行」);highlight 例在第 63 段(text/14-ch06.txt:63,搜「highlight」)。

  5. 出处:「第一组重构」第 552 段(text/14-ch06.txt:552,搜「更宽的上下文中也有意义」)。

  6. 出处:「第一组重构」第 105 段(text/14-ch06.txt:105,搜「先放弃提炼」)。

  7. 出处:「第一组重构」第 389 段(text/14-ch06.txt:389,搜「挑选另一块代码来提炼」)。

  8. 出处:「第一组重构」第 421 段(text/14-ch06.txt:421,搜「间接性可能带来帮助」)。

  9. 出处:「第一组重构」第 428 段(text/14-ch06.txt:428,搜「间接层有其价值」)。

  10. 出处:「第一组重构」第 701 段(text/14-ch06.txt:701,搜「并不比表达式本身更具表现力」)。

  11. 出处:「第一组重构」第 734 段(text/14-ch06.txt:734,搜「软件系统的关节」)。

  12. 出处:「第一组重构」第 744 段(text/14-ch06.txt:744,搜「再把这句注释变成函数的」)。

  13. 出处:「第一组重构」第 764 段(text/14-ch06.txt:764,搜「没有正确答案」)。

  14. 出处:「第一组重构」第 940 段(text/14-ch06.txt:940,搜「inNewEngland」)、第 970 段(text/14-ch06.txt:970,搜「xxNEWinNewEngland」)与第 999 段(text/14-ch06.txt:999,搜「stateCode」)。

  15. 出处:「第一组重构」第 871 段(text/14-ch06.txt:871,搜「快乐的终点」);deprecated 流程在第 811-813 段(text/14-ch06.txt:811,搜「已对外发布的API」)。

  16. 出处:「第一组重构」第 1030 段(text/14-ch06.txt:1030,搜「重新组织函数」)。

  17. 出处:「第一组重构」第 1034 段(text/14-ch06.txt:1034,搜「作用域超出单个函数」)。

  18. 出处:「第一组重构」第 1047 段(text/14-ch06.txt:1047,搜「代码防腐剂」)。 2

  19. 出处:「第一组重构」第 1288 段(text/14-ch06.txt:1288,搜「更深层次的改变」)。

  20. 出处:「第一组重构」第 1456 段(text/14-ch06.txt:1456,搜「形影不离地操作同一块数据」)。

  21. 出处:「第一组重构」第 1651 段(text/14-ch06.txt:1651,搜「对源数据做更新」)。

  22. 出处:「第一组重构」第 1850 段(text/14-ch06.txt:1850,搜「两件不同的事」)。

  23. 出处:「第一组重构」第 1866 段(text/14-ch06.txt:1866,搜「最明显的线索」)。

  24. 出处:「第一组重构」第 1003 段(text/14-ch06.txt:1003,搜「自动化重构工具减少了迁移式做法」)。