搬移:把代码送到它归属的上下文
1. 这一章讲什么
坏味道罗盘里最需要时间才能闻到的几味(依恋情结、霰弹式修改),处方全在本章:搬移。前两章的手法是「在原地给代码塑形」,这一章的手法是「让代码换个地方住」。核心判断只有一个:这块代码跟谁最亲?原书章首把本章手法分成两层——在上下文之间搬(函数、字段),在函数内外搬(语句),外加两组特殊对象:循环和死代码。
2. 顶层全景
谁跟谁亲?
函数频繁引用别处的元素 ──→ 搬移函数(连它依赖的一起搬)
字段总被别处的记录一起用 ──→ 搬移字段
只是几行的顺序不对 ──→ 移动语句 / 搬移语句到函数 / 搬移语句到调用者
一个循环干几件事 ──→ 拆分循环 → (可选)以管道取代循环
代码做的事已有现成函数 ──→ 以函数调用取代内联代码
代码没人用了 ──→ 移除死代码
图说:前两行是「大件搬家」,中间一行是「整理房间」,后两行是「换家具」和「扔垃圾」。
3. 大件搬家:搬移函数与搬移字段
搬移函数的地基是模块化:「好的模块化能够让我在修改程序时只需理解程序的一小部分」;而理解在加深,「要将这种理解反映到代码上,就得不断地搬移这些元素」1。最直接的动因写得很形象:函数「频繁引用其他上下文中的元素,而对自身上下文中的元素却关心甚少」,那就「让它去与那些更亲密的元素相会」2。搬的时候先检查依赖:它引用了谁、谁引用它;通常先搬它调用的其他函数——「总是从依赖最少的那个函数入手」。
操作上有一条 JS 版的宽慰:搬移通常意味着新名字、新参数,而现代工具让这一切大多可自动完成;搬完源函数先改成一行委托再内联,路就断不了。
搬移字段的理由更深一层:「往往数据结构才是一个健壮程序的根基」——好的数据结构让行为代码简单明了,坏的数据结构让代码在结构之间「纠缠不清」3。但数据结构很难一次做对,作者的原话带着时间感:「这个星期看来合理而正确的设计决策,到了下个星期可能就不再正确了」——所以要有一套随手修正数据结构的技法4。什么时候搬:调用某个函数时总要多传另一条记录的字段;改一条记录总牵动另一条。对类的表述很精辟:「类只是一种多了实例函数的记录」——所以封装过的数据搬起来容易,只改访问函数,客户端无感5。
判断块(我们的,不是书里的):「决定越难做,通常说明“搬移这个函数与否”的重要性也越低」6——这条反直觉的经验值得单独记住:纠结本身是低收益信号,先安置、不合适再搬,比停在原地权衡便宜。如果错,会错在: 如果搬移的代码处在 API 边界上(搬错代价大),这条经验不适用;原书同一段也说了要对「调用者都有谁」做检查。
4. 语句级搬家:三种搬法
移动语句是最小的搬法:「让存在关联的东西一起出现,可以使代码更容易理解」;它还是提炼函数的准备动作——相关代码不聚拢,函数提炼无从下手7。安全判据一句话:副作用。没有副作用的代码,几乎可以随便编排(任意安排先后)顺序——作者原话:「我几乎可以随心所欲地编排它们的顺序」;反过来说,挪动有副作用的语句之前,必须检查跨过的每一行8。原书例子是一段 13 行的计价代码:纯粹的变量声明随便挪,而「取定价方案」这类函数调用是否可挪,要一路检查到调用链底端——这也是「优秀的程序员都会尽量编写无副作用代码」的实际回报9。
搬移语句到函数管重复:调用某函数时总有一段相同代码跟着执行,就把它合并进函数——日后修改一处,所有调用者同时生效。前提是这段代码对每个调用者都该执行;如果将来有人不需要,再反向搬出来就是。搬移语句到调用者管的是另一种时间病:抽象边界的漂移。曾经「视为一个整体」的行为——一个单元(自成一块的部分)——如今可能已经分化出两个甚至多个不同的关注点。典型征兆是某个共用的行为在个别调用点需要不同表现,就把差异部分搬回调用处,让它各自演化10。作者给它划了适用边界:只治「边界仅有些许偏移」;偏得太远就别缝了,先内联合并、再重新提炼出新的边界11。
5. 循环:先拆分,再管道化
循环值得单独一组手法,因为它太容易身兼多职:「它们一次做了两三件事情,不为别的,就因为这样可以只循环一次」12。拆分循环的理由:一个循环干两件事,改它时就得同时理解两件事;拆开后每个循环只算一个值,直接 return,为下一步提炼函数铺路。对「跑两遍循环」的性能不安,作者的回答和第 01 章同一口径:「循环本身也很少成为性能瓶颈」,拆开反而让更强大的优化成为可能13。原书例子:一个循环同时算最年轻员工年龄和工资总额,拆成两个各司其职。
以管道取代循环则更进一步,把循环整个消灭:集合管道(collection pipeline)「允许我使用一组运算来描述集合的迭代过程」,最常见的两节是 map(逐个变换)和 filter(筛选)14。可读性收益被作者说得很具体:「我只消从头到尾阅读一遍代码,就能弄清对象在管道中间的变换过程」——而嵌套循环要靠脑子模拟运行15。原书走查:一个从 CSV(逗号分隔的文本表格)里挑出印度办公室(城市+电话)的循环,带跳表头、跳空行、record[1].trim() === "India" 判断三件事,逐步换成 slice(1)、filter、map 的管道,每一行的意图都写在脸上。
6. 换家具与扔垃圾
以函数调用取代内联代码:见到内联代码在做某个现成函数已做的事,就换成函数调用——「一个命名良好的函数,本身就能极好地解释代码的用途」,还白赚消除重复16。一个防坑判据:内联代码与函数可能只是「外表相似但其实并无本质联系」;检验法是看函数名放进这段代码的语境协不协调——不协调就别换17。
移除死代码是「任何一个合格程序员的至爱」18。它的成本模型很清楚:死代码不拖慢程序(编译器甚至会删掉它),成本全在阅读上——死代码没有标记(标明「可放心跳过」的记号),作者说「它们周围没有任何警示或标记能告诉程序员,让他们能够放心忽略这段函数」19。
删除的底气来自版本控制:「一旦代码不再被使用,我们就该立马删除它」;真需要时从版本库(保存全部历史的系统)里翻找回来就是。「注释掉它」是版本控制不普及年代的遗俗,如今已无理由20。
7. 作者的判断与证据
| 判断 | 证据 |
|---|---|
| 数据结构是根基,但「下个星期可能就不再正确」 | 经验立场,与第 02 章「设计浮现」一致4 |
| 循环两遍几乎不伤性能 | 与 2.8 节的 90% 论同源(第 12 章展开)13 |
| 管道优于循环 | 语言演进的事实(函数成为一等公民)+可读性论证 |
| 死代码该立刻删 | 成本收益论证:阅读负担 vs 版本控制兜底19 |
判断(我们的,不是书里的): 本章所有手法共享一个前置诊断——「归属」。搬移函数搬的是职责,搬移字段搬的是数据主权,移动语句搬的是依赖的顺序;手法本身都简单,难的全在判断「该搬向哪」,而书给的判据(依恋谁、谁的数据最多、哪个方向在变)都指向同一个动作:数一数引用关系。如果错,会错在: 引用关系密集的遗留代码里,数出来的结果可能互相矛盾(跟 A 亲也跟 B 亲),那时该先拆阶段而不是先搬家。
8. 边界与局限
- 管道取代循环依赖语言有函数式集合操作;没有的语言里,这半章手法直接不可用。
- 拆分循环对「必须一次遍历」的流式场景不适用(原书未展开,我们补边界)。
- 搬移语句到调用者在多态体系里要多走一步:每个子类的覆写版本都要做同样的提炼。
- 移除死代码的前提是「真的死」:反射调用、动态拼名等间接引用会让「查找调用点」失灵——工具章节会回到这个坑。
9. 可带走的
- 函数跟谁的数据最亲,就搬到谁家;先搬依赖最少的。
- 改一条记录总牵动另一条,是字段放错位置的信号。
- 挪语句前先问:跨过的这些行有没有副作用?没有才随便挪。
- 共用行为在个别调用点开始「不一样」时,把差异搬回调用点。
- 循环干两件事就拆成两个;语言支持的话,继续换成 filter/map。
- 有现成函数就别重写一遍,但要确认是真重复,不是外表像。
- 死代码立刻删;注释掉的代码是这个时代的笑话。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 移除死代码是至爱 | 第8章 开篇 | text/16-ch08.txt:15(搜「合格程序员的至爱」) |
| 模块化与搬移 | 第8章 §8.1 | text/16-ch08.txt:28(搜「优秀软件设计的核心」) |
| 更亲密的元素 | 第8章 §8.1 | text/16-ch08.txt:39(搜「最直接的一个动因」) · text/16-ch08.txt:40(搜「更亲密的元素相会」) |
| 去处难定 | 第8章 §8.1 | text/16-ch08.txt:51(搜「重要性也越低」) |
| 数据结构是根基 | 第8章 §8.2 | text/16-ch08.txt:419(搜「往往数据结构才是一个健壮程序的」) |
| 下个星期不再正确 | 第8章 §8.2 | text/16-ch08.txt:429(搜「下个星期可能就不再正确」) |
| 类是多了函数的记录 | 第8章 §8.2 | text/16-ch08.txt:447(搜「多了实例函数的记录」) |
| 黄金守则 | 第8章 §8.3 | text/16-ch08.txt:662(搜「黄金守则」) |
| 边界偏移 | 第8章 §8.4 | text/16-ch08.txt:810(搜「悄无声息地发生偏移」) · text/16-ch08.txt:811(搜「曾经视为一个整体」) |
| 外表相似 | 第8章 §8.5 | text/16-ch08.txt:1035(搜「外表相似」) |
| 移动语句 | 第8章 §8.6 | text/16-ch08.txt:1064(搜「让存在关联的东西一起出现」) · text/16-ch08.txt:1130(搜「随心所欲地编排」) |
| 拆分循环 | 第8章 §8.7 | text/16-ch08.txt:1239(搜「身兼多职的循环」) · text/16-ch08.txt:1251(搜「很少成为性能瓶颈」) |
| 管道取代循环 | 第8章 §8.8 | text/16-ch08.txt:1373(搜「集合管道」) · text/16-ch08.txt:1378(搜「从头到尾阅读一遍代码」) |
| 移除死代码 | 第8章 §8.9 | text/16-ch08.txt:1609(搜「思维负担」) · text/16-ch08.txt:1613(搜「立马删除它」) · text/16-ch08.txt:1619(搜「注释掉它」) |