<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>重构 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E9%87%8D%E6%9E%84/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Tue, 08 Aug 2023 10:40:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E9%87%8D%E6%9E%84/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>《重构》学习总结：改善既有代码的设计，把日课做成习惯</title>
      <link>https://ftmao.com/blog/bookstore/refactoring-fowler-book-notes/</link>
      <pubDate>Tue, 08 Aug 2023 10:40:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/bookstore/refactoring-fowler-book-notes/</guid>
      <description>&lt;h1 id=&#34;重构学习总结改善既有代码的设计把日课做成习惯&#34;&gt;《重构》学习总结：改善既有代码的设计，把日课做成习惯&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：写过一两年业务代码、经常被祖传代码折磨、想搞清楚“重构到底怎么落地”的程序员。&lt;br&gt;
参考书籍：Martin Fowler《重构：改善既有代码的设计（第 2 版）》（Refactoring, 2nd Edition）&lt;br&gt;
阅读目标：建立“重构是刷牙不是大扫除”的日常习惯，把坏味道清单变成自己的启发法清单，学会在安全网保护下小步快走。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;0-写在前面重构不是大扫除是刷牙&#34;&gt;0. 写在前面：重构不是大扫除，是刷牙&lt;/h2&gt;
&lt;p&gt;很多人对重构的想象是这样的：找一个安静的下午，拉上需求方开个会，申请两周“专项重构时间”，然后把某个模块推倒重来。这种理解恰恰是 Fowler 最反对的。&lt;/p&gt;
&lt;p&gt;这本书最核心的一个比喻，可以用我自己的话总结为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重构不是年底大扫除，是每天刷牙。大扫除轰轰烈烈、一年一次、中间家里一直脏；刷牙小额、频繁、不痛不痒，但牙齿几十年不出大问题。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个比喻对应到工程上有三个推论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;小额&lt;/strong&gt;：一次重构就改一点点，编译能过、测试能绿，马上提交。不是攒一个月再交一个巨型 PR。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;频繁&lt;/strong&gt;：每次加功能、改 bug、做 code review 的时候顺手做，融进日常工作流，不需要“立项”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不改变外在行为&lt;/strong&gt;：这是定义，不是建议。下面马上展开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么这本书值得每个程序员精读？因为&lt;strong&gt;坏味道清单就是启发法清单的代码版&lt;/strong&gt;。算法书教你遇到问题时用什么启发法求解，Fowler 教你看到代码时用什么启发法判断“这里不对劲”以及“该用哪招”。读熟之后，你看代码的眼光会永久改变——这是少数几本读一遍就能立刻涨功力的书。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-一句话理解重构不改变可观察行为的结构调整&#34;&gt;1. 一句话理解重构：不改变可观察行为的结构调整&lt;/h2&gt;
&lt;p&gt;Fowler 给出的定义（第 2 版，第 2 章）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重构（名词）：对软件内部结构的一种调整，目的是在不改变软件可观察行为的前提下，提高其可理解性，降低其修改成本。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;拆开看有三个关键词：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;结构调整&lt;/strong&gt;：动的是代码的组织方式——函数怎么拆、类怎么分、数据放哪里。不是改它“做什么”，是改它“怎么摆”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不改变可观察行为&lt;/strong&gt;：用户看到的、API 返回的、数据库写入的，统统不能变。你改了行为，那叫“改功能”或“修 bug”，不叫重构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可理解性 / 修改成本&lt;/strong&gt;：重构的目标从来不是“代码更好看”这种审美追求，而是&lt;strong&gt;让下一次修改更便宜&lt;/strong&gt;。这是经济学决策，不是艺术决策。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;11-两顶帽子加功能和重构不同时戴&#34;&gt;1.1 两顶帽子：加功能和重构不同时戴&lt;/h3&gt;
&lt;p&gt;Fowler 引用了 Kent Beck 的“两顶帽子”比喻，这是全书我最常拿来讲给同事听的概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;戴上“&lt;strong&gt;加功能&lt;/strong&gt;”的帽子：你只添加新代码、新测试，&lt;strong&gt;绝不修改已有代码的结构&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;戴上“&lt;strong&gt;重构&lt;/strong&gt;”的帽子：你只调整结构、保证测试全绿，&lt;strong&gt;绝不添加新功能&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写代码的每一刻，你头上只能戴一顶帽子。发现“要加这个功能，得先把这段代码理顺”，正确动作是：摘下加功能的帽子 → 戴上重构帽子 → 重构到方便加功能为止 → 换回加功能帽子 → 加功能。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;接到需求：要加新功能&amp;#34;] --&amp;gt; B{&amp;#34;现有代码结构&amp;lt;br/&amp;gt;方便加这个功能吗？&amp;#34;}
    B -- &amp;#34;方便&amp;#34; --&amp;gt; C[&amp;#34;戴加功能的帽子&amp;lt;br/&amp;gt;写新代码 + 新测试&amp;#34;]
    B -- &amp;#34;不方便&amp;#34; --&amp;gt; D[&amp;#34;戴重构的帽子&amp;lt;br/&amp;gt;只做结构调整，测试保持全绿&amp;#34;]
    D --&amp;gt; E{&amp;#34;结构理顺了吗？&amp;#34;}
    E -- &amp;#34;还没有&amp;#34; --&amp;gt; D
    E -- &amp;#34;理顺了&amp;#34; --&amp;gt; C
    C --&amp;gt; F[&amp;#34;提交，帽子归位&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;为什么这么严格？因为&lt;strong&gt;同时做两件事，出了 bug 你分不清是谁引入的&lt;/strong&gt;。重构的每一步都要求测试全绿，如果中途改了行为，红灯亮起时你就失去了判断依据。两顶帽子是重构能保持“小步”的心理学保障：它把一件模糊的、容易失控的事，拆成了两个界限分明的状态。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-坏味道清单程序员的启发法清单&#34;&gt;2. 坏味道清单：程序员的启发法清单&lt;/h2&gt;
&lt;p&gt;第 3 章是全书的“字典”——24 种坏味道（Bad Smells）。注意 Fowler 的用词是“味道”不是“罪状”：&lt;strong&gt;闻到味道不代表必须立刻处理，它只是提示你“这里值得看一眼”&lt;/strong&gt;。极端的教条主义本身就是一种味道。&lt;/p&gt;
&lt;p&gt;24 种味道完整清单如下，后面挑最重要的 10 个深入讲：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;神秘命名、重复代码、过长函数、过长参数列表、全局数据、可变数据、发散式变化、霰弹式修改、依恋情结、数据泥团、基本类型偏执、重复的 switch、循环、冗赘的元素、夸夸其谈未来性、临时字段、过长的消息链、中间人、内幕交易、过大的类、异曲同工的类、纯数据类、被拒绝的遗赠、注释。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;21-神秘命名mysterious-name&#34;&gt;2.1 神秘命名（Mysterious Name）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;味道&lt;/strong&gt;：变量叫 &lt;code&gt;data&lt;/code&gt;、函数叫 &lt;code&gt;process&lt;/code&gt;、类叫 &lt;code&gt;Manager&lt;/code&gt;，看完名字不知道它是干嘛的。&lt;/p&gt;
&lt;p&gt;为什么排第一？因为&lt;strong&gt;命名是程序里密度最高的文档&lt;/strong&gt;。一段代码你可能读十次改一次，名字起得好，十次阅读每次都省时间。Fowler 说得直白：如果想不到好名字，往往意味着你对这段逻辑的职责还没想清楚——命名困难本身就是设计问题的信号。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对应手法&lt;/strong&gt;：改变函数声明（改名）、变量改名、字段改名。现代 IDE 的重命名重构是安全的，改名字没有借口留到下辈子。&lt;/p&gt;
&lt;h3 id=&#34;22-重复代码duplicated-code&#34;&gt;2.2 重复代码（Duplicated Code）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;味道&lt;/strong&gt;：同一段逻辑出现在两个以上的地方。改一处忘另一处，bug 就是这么来的。&lt;/p&gt;
&lt;p&gt;这是所有味道里&lt;strong&gt;危害最确定、修复手法最成熟&lt;/strong&gt;的一个。双胞胎函数用提炼函数收拢；兄弟子类里重复的用函数上移；类似但不完全相同的，先拆出相同的部分再收拢。&lt;/p&gt;
&lt;p&gt;一个工程界的经典注脚：NASA 的航天飞机软件团队以极低缺陷率著称，他们的经验之一就是极端重视消除重复——任何相似代码都必须解释为什么存在。日常项目虽然不必如此严苛，但“重复三次就提炼”的底线值得守住。&lt;/p&gt;
&lt;h3 id=&#34;23-过长函数long-function&#34;&gt;2.3 过长函数（Long Function）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;味道&lt;/strong&gt;：一个函数几十上百行，得靠注释分段才能读懂。&lt;/p&gt;
&lt;p&gt;Fowler 的态度非常激进：&lt;strong&gt;函数就该短&lt;/strong&gt;。短函数的好处是&lt;strong&gt;通过名字传递意图&lt;/strong&gt;——&lt;code&gt;calcOutstanding()&lt;/code&gt; 比一坨内联计算强得多，因为读者看名字就知道意图，需要细节时才钻进去。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;判断标准&lt;/strong&gt;：不是行数，而是“&lt;strong&gt;如果你需要写注释解释一段代码在干嘛，这段代码就该被提炼成函数，函数名用注释里的词&lt;/strong&gt;”。这个标准极其好用，直接把“注释”这个味道变成了提炼函数的路标。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对应手法&lt;/strong&gt;：提炼函数（Extract Function）——全书使用频率最高的手法，没有之一。第 2 版里 Fowler 把提炼后的函数长度做到五六行甚至两三行，就是要打破“函数太短不值得”的心理障碍。&lt;/p&gt;
&lt;h3 id=&#34;24-过长参数列表long-parameter-list&#34;&gt;2.4 过长参数列表（Long Parameter List）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;味道&lt;/strong&gt;：函数签名里七八个参数，调用处像咒语。&lt;/p&gt;
&lt;p&gt;危害有两个：一是调用方每次都要记参数顺序，极易传错（尤其是同类型相邻参数）；二是参数多往往说明&lt;strong&gt;函数在做不止一件事&lt;/strong&gt;，或者这些参数本该是一个整体。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对应手法&lt;/strong&gt;：以查询取代参数（能从参数 A 算出参数 B，就别传 B）、保持对象完整（别从对象里掏三个字段传进去，直接传对象）、引入参数对象（见 2.7 数据泥团）、移除标记参数（布尔 flag 参数往往意味着函数里有个 if 分叉，拆成两个函数更干净）。&lt;/p&gt;
&lt;h3 id=&#34;25-全局数据global-data与可变数据mutable-data&#34;&gt;2.5 全局数据（Global Data）与可变数据（Mutable Data）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;全局数据的味道&lt;/strong&gt;：任何地方都能读到、改到它，出了问题你找不到是谁改的。单例本质上也是全局数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可变数据的味道&lt;/strong&gt;：一个变量在很大作用域内被反复赋值，你在第 500 行看到的值，无法信任它声明时的样子。&lt;/p&gt;
&lt;p&gt;这对兄弟是并发 bug 和“灵异 bug”的头号来源。函数式编程这些年崛起的核心卖点之一就是消灭可变状态，和 Fowler 的判断完全同频。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对应手法&lt;/strong&gt;：封装变量（把全局变量关进函数里，访问必须经过函数）、拆分变量（一个变量被赋两次值干两件事，就拆成两个）、将引用对象改为值对象（值对象不可变，天然免疫一大类 bug）。&lt;/p&gt;
&lt;h3 id=&#34;26-发散式变化与霰弹式修改divergent-change--shotgun-surgery&#34;&gt;2.6 发散式变化与霰弹式修改（Divergent Change / Shotgun Surgery）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;发散式变化&lt;/strong&gt;：&lt;strong&gt;一个类因为不同原因被反复修改&lt;/strong&gt;。“数据库变了改它，报表格式变了也改它，业务规则变了还改它”——这个类承担了太多职责。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;霰弹式修改&lt;/strong&gt;：&lt;strong&gt;一个变化要改很多个类&lt;/strong&gt;。加一个字段类型，DAO 改、Service 改、DTO 改、前端 mock 改，漏一个就炸。&lt;/p&gt;
&lt;p&gt;这俩是&lt;strong&gt;同一枚硬币的两面&lt;/strong&gt;：前者是“多个原因指向一个类”，后者是“一个原因散到多个类”。修复方向相反——发散式变化用&lt;strong&gt;拆分阶段、搬移函数&lt;/strong&gt;把一个类拆开；霰弹式修改用&lt;strong&gt;搬移函数、提炼类&lt;/strong&gt;把散落的逻辑聚到一处。&lt;/p&gt;
&lt;p&gt;判断技巧：想象需求来了，你心里默念“要改哪几个文件”。如果答案是“又是这个巨无霸类”，是发散式变化；如果答案是“天哪又是七八个文件”，是霰弹式修改。&lt;/p&gt;
&lt;h3 id=&#34;27-依恋情结与数据泥团feature-envy--data-clumps&#34;&gt;2.7 依恋情结与数据泥团（Feature Envy / Data Clumps）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;依恋情结&lt;/strong&gt;：一个函数对另一个类的数据兴趣远大于自己所在类的——函数里 &lt;code&gt;order.getCustomer().getAddress().getCity()&lt;/code&gt; 一长串，全是别人家的 getter。治疗方式很直白：&lt;strong&gt;搬移函数&lt;/strong&gt;，让函数搬到它依恋的数据旁边去。“策略”“访问者”模式是例外，它们故意把行为和数据分开。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据泥团&lt;/strong&gt;：总是一起出现的三四个字段——&lt;code&gt;startDate&lt;/code&gt;/&lt;code&gt;endDate&lt;/code&gt;、&lt;code&gt;street&lt;/code&gt;/&lt;code&gt;city&lt;/code&gt;/&lt;code&gt;zip&lt;/code&gt;。它们像连体婴儿一样在各个方法签名和类字段里成群结队。&lt;strong&gt;对应手法&lt;/strong&gt;：提炼类，给这团数据一个家（比如 &lt;code&gt;DateRange&lt;/code&gt;、&lt;code&gt;Address&lt;/code&gt;）。提炼完常常有惊喜：散落各处的校验、格式化逻辑（依恋情结）会自然地搬进新类。&lt;/p&gt;
&lt;h3 id=&#34;28-基本类型偏执与重复的-switchprimitive-obsession--repeated-switches&#34;&gt;2.8 基本类型偏执与重复的 switch（Primitive Obsession / Repeated Switches）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;基本类型偏执&lt;/strong&gt;：用字符串存电话号码、用裸 double 存钱、用 int 编码状态。小钱变大钱、状态码幻数满天飞，都是这么来的。&lt;strong&gt;对应手法&lt;/strong&gt;：以对象取代基本类型——一个 &lt;code&gt;Money&lt;/code&gt; 类、&lt;code&gt;PhoneNumber&lt;/code&gt; 类，把校验和格式化的知识收拢进类型系统。业界血案：金额用 float/double 导致的分分钱对不上账，几乎每个支付团队都踩过，标准做法就是专门的 Money 类型或以最小货币单位为单位的整数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重复的 switch&lt;/strong&gt;：同一个类型码的 &lt;code&gt;switch&lt;/code&gt;/&lt;code&gt;if-else&lt;/code&gt; 链散落多处。每加一种新类型，得把所有 switch 都找出来改一遍——这是霰弹式修改的特化版。&lt;strong&gt;对应手法&lt;/strong&gt;：&lt;strong&gt;以多态取代条件表达式&lt;/strong&gt;（见 3.6 节）。注意 Fowler 的克制：switch 本身不是罪，&lt;strong&gt;重复的&lt;/strong&gt; switch 才是；如果只有一个 switch 且类型稳定，留着无妨。&lt;/p&gt;
&lt;h3 id=&#34;29-中间人与消息链middle-man--message-chains&#34;&gt;2.9 中间人与消息链（Middle Man / Message Chains）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;中间人&lt;/strong&gt;：一个类一半以上的方法都在委托给别的类，自己什么也不干。封装是好事，但封装到这个程度就是纯粹的过路费。&lt;strong&gt;对应手法&lt;/strong&gt;：移除中间人，让调用方直接和被委托对象说话。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;消息链&lt;/strong&gt;：&lt;code&gt;a.getB().getC().getD()&lt;/code&gt; 一路要下去，调用方对整条链的结构产生了依赖，链中间任何一环变动都会传导过来。这就是“得墨忒耳定律”（Law of Demeter）说的“只和直接朋友说话”。&lt;strong&gt;对应手法&lt;/strong&gt;：隐藏委托关系。&lt;/p&gt;
&lt;p&gt;这一对味道放在一起看很有意思：&lt;strong&gt;过犹不及&lt;/strong&gt;。委托太多是中间人，穿透太多是消息链， Fowler 的词典里没有银弹，只有“闻味道、看剂量”。&lt;/p&gt;
&lt;h3 id=&#34;210-注释comments&#34;&gt;2.10 注释（Comments）&lt;/h3&gt;
&lt;p&gt;注释也上榜了，很多人第一次读会惊讶。Fowler 的逻辑是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注释本身不是坏味道，它常常是“ deodorant（除臭剂）”——用来遮盖代码本身的臭。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当你觉得“这里得写段注释解释一下”，先问：&lt;strong&gt;能不能提炼函数用函数名说清楚？能不能改变量名说清楚？&lt;/strong&gt; 能，就别写注释。注释该写的是“为什么这么做”（业务背景、历史坑、外部约束），而不是“这段代码在干嘛”——后者是代码没写清楚的忏悔。&lt;/p&gt;
&lt;h3 id=&#34;211-其余味道速查表&#34;&gt;2.11 其余味道速查表&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;味道&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;一句话识别&lt;/th&gt;
					&lt;th style=&#34;text-align: left&#34;&gt;首选手法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;循环（Loops）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;for 循环里干的事能用管道表达&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;以管道取代循环（filter/map/reduce）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;冗赘的元素（Lazy Element）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;这个函数/类就只有一层转发，毫无增量&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;内联函数、内联类&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;夸夸其谈未来性（Speculative Generality）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;“为将来预留”的抽象，现在只有一个实现&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;折叠继承体系、内联&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;临时字段（Temporary Field）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;某个字段只在特定算法运行时才有值&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;提炼类 + 搬移函数，给它一个家&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;内幕交易（Insider Trading）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;两个模块私下大量交换数据，耦合紧密&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;搬移函数/字段，减少私聊&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;过大的类（Large Class）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;一个类字段太多、职责太多&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;提炼类、提炼超类&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;异曲同工的类（Alternative Classes）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;两个类接口不同干的事一样&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;改变函数声明 + 搬移函数统一接口&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;纯数据类（Data Class）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;只有字段和 getter/setter，没有行为&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;把操作它的行为搬进来（封装记录）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;被拒绝的遗赠（Refused Bequest）&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;子类继承了一堆用不上的东西&lt;/td&gt;
					&lt;td style=&#34;text-align: left&#34;&gt;函数/字段下移，或以委托取代继承&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;“夸夸其谈未来性”值得单独敲一下黑板：&lt;strong&gt;YAGNI（You Aren&amp;rsquo;t Gonna Need It）&lt;/strong&gt;。每个“将来可能用得上”的抽象，都是要付利息的技术债——它现在就开始收阅读成本，而兑现的那一天往往永远不会来。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;3-核心重构手法七招走天下&#34;&gt;3. 核心重构手法：七招走天下&lt;/h2&gt;
&lt;p&gt;第 6 章起的重构名录有 60 多个手法，但日常高频使用的集中在少数几招。下面七招是我个人使用频率最高、也最能体现“小步安全”思想的。&lt;/p&gt;
&lt;h3 id=&#34;31-提炼函数extract-function&#34;&gt;3.1 提炼函数（Extract Function）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：一段代码需要注释才能看懂；或者一个函数太长。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;口诀&lt;/strong&gt;：&lt;strong&gt;意图与实现分离&lt;/strong&gt;——“这段代码做什么”（意图）交给函数名，“怎么做”（实现）交给函数体。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;小步版本&lt;/strong&gt;（Fowler 的名录里每个手法都有这种机械化步骤，这是本书最容易被低估的价值）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建新函数，按意图命名（想不到好名字就先叫临时名，回头再改）。&lt;/li&gt;
&lt;li&gt;把要提炼的代码&lt;strong&gt;复制&lt;/strong&gt;过去（是复制，不是剪切——先留着原件，错了好回退）。&lt;/li&gt;
&lt;li&gt;检查局部变量：被新函数读到的 → 传参；被修改的 → 尽量变成返回值，一次只返回一个。&lt;/li&gt;
&lt;li&gt;编译。在源位置替换为对新函数的调用。&lt;/li&gt;
&lt;li&gt;测试。全绿则提交。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;避坑&lt;/strong&gt;：一次提炼只处理“只读变量多、被改变量少”的块；被改变量超过一个时，先用“拆分变量”或调整结构再提炼。&lt;/p&gt;
&lt;h3 id=&#34;32-内联函数inline-function&#34;&gt;3.2 内联函数（Inline Function）&lt;/h3&gt;
&lt;p&gt;提炼的反操作。&lt;strong&gt;场景&lt;/strong&gt;：函数体已经和函数名一样清楚（&lt;code&gt;getRating() { return moreThanFiveLateDeliveries ? 2 : 1; }&lt;/code&gt; 改名后可能直接内联掉），或者一堆小函数间接层太深、改名/重组前需要先“摊平”。&lt;/p&gt;
&lt;p&gt;重构名录里的手法大多成对出现（提炼/内联、搬入/搬出、封装/暴露），这不是凑数——&lt;strong&gt;可逆性是小步快走的前提&lt;/strong&gt;：这步走错了，反手一步就能退回来。&lt;/p&gt;
&lt;h3 id=&#34;33-搬移函数move-function&#34;&gt;3.3 搬移函数（Move Function）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：函数引用别的上下文的元素多于自己所在的上下文（依恋情结的特效药）；或者模块化需要重新划分边界。&lt;/p&gt;
&lt;p&gt;口诀：&lt;strong&gt;逻辑和它操作的数据住在一起&lt;/strong&gt;。搬移时先检查函数引用的所有东西——它们要么一起搬，要么变成参数。搬完旧位置可以留一个委托函数过渡，等调用方都改完再删。&lt;/p&gt;
&lt;h3 id=&#34;34-提炼变量extract-variable&#34;&gt;3.4 提炼变量（Extract Variable）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：表达式又长又复杂，比如：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;quantity&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;itemPrice&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;max&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;quantity&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;500&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;itemPrice&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.05&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;min&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;quantity&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;itemPrice&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;逐个起名字：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;basePrice&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;quantity&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;itemPrice&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;quantityDiscount&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;max&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;quantity&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;500&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;itemPrice&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.05&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;shipping&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;min&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;basePrice&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;basePrice&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;quantityDiscount&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;shipping&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;零风险、几秒钟、可读性立刻翻倍。&lt;strong&gt;这是性价比最高的重构手法&lt;/strong&gt;，新人最值得先养成这个习惯。变量命名还能顺带自我解释业务规则：原来那 0.05 是批量折扣，0.1 是运费比例。&lt;/p&gt;
&lt;h3 id=&#34;35-封装记录encapsulate-record&#34;&gt;3.5 封装记录（Encapsulate Record）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：代码里到处传裸的 object/map/dict（纯数据类的输入版），字段名靠口口相传。&lt;/p&gt;
&lt;p&gt;把记录变成类，访问走方法：字段名写错编译期就炸，而不是线上拿到 &lt;code&gt;undefined&lt;/code&gt;；还能给计算字段提供方法（&lt;code&gt;total&lt;/code&gt; 不必存，用 &lt;code&gt;get total()&lt;/code&gt; 算）。这是 TypeScript 兴起后整个行业都在做的事——&lt;strong&gt;让类型系统替你站岗&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;36-以多态取代条件表达式replace-conditional-with-polymorphism&#34;&gt;3.6 以多态取代条件表达式（Replace Conditional with Polymorphism）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：同一个类型码的 switch 散落多处（重复的 switch 味道）；或者一个函数内部按类型分叉，分支逻辑还会持续增加。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步骤&lt;/strong&gt;：为每种类型建子类 → 把条件分支的逻辑搬成子类的覆写方法 → 原函数变抽象。书里的经典例子是“鸟类评级”：按鸟的种类（欧洲燕子、非洲燕子、挪威蓝鹦鹉）分支计算飞行速度和评级，重构后每种鸟一个子类，新增鸟种只需新增一个类——&lt;strong&gt;修改从“找到所有 switch”变成“新建一个文件”&lt;/strong&gt;，这正是开闭原则。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;避坑&lt;/strong&gt;：类型码&lt;strong&gt;稳定且数量少&lt;/strong&gt;时不要硬上多态。switch 只有一处、类型十年不变，多态带来的类爆炸是另一种过度设计。再强调一次：闻味道，看剂量。&lt;/p&gt;
&lt;h3 id=&#34;37-拆分阶段split-phase&#34;&gt;3.7 拆分阶段（Split Phase）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：一段代码同时干两件性质不同的事——典型的如“解析输入 + 业务计算 + 输出格式”。&lt;/p&gt;
&lt;p&gt;把两段拆开，中间用一个明确的数据结构传递：第一阶段产出中间对象，第二阶段消费它。好处是&lt;strong&gt;两个阶段的修改互不影响&lt;/strong&gt;——换输出格式不动计算逻辑，换输入来源不动下游。书中 statement 案例（下节）的核心一步就是拆分阶段。命令行工具、编译器（解析→语义分析→代码生成）、数据管道，全是这个手法的工业级应用。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;4-全书导读案例statement-计费重构走查&#34;&gt;4. 全书导读案例：statement 计费重构走查&lt;/h2&gt;
&lt;p&gt;第 2 版第 1 章用 JavaScript 完整走查了一个案例，非常值得复现一遍，因为它是“小步快走”的教科书级演示。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原始代码&lt;/strong&gt;：一个剧团的账单系统，&lt;code&gt;statement(invoice, plays)&lt;/code&gt; 输出客户看戏的账单文本。一个约 40 行的函数，里面一个 switch 按戏剧类型（悲剧/喜剧）算钱，外加积分计算和字符串拼接，全部揉在一起。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需求来了&lt;/strong&gt;（这就是重构的触发器）：① 账单要支持 HTML 输出；② 剧团打算演更多类型的戏（比如“历史剧”“田园剧”），算钱和积分规则要可扩展。&lt;/p&gt;
&lt;p&gt;Fowler 没有直接动手写 HTML 版本，而是先重构。整个过程的关键步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先补测试&lt;/strong&gt;：原代码没有任何测试。第一步不是改代码，是给现有行为写一批测试，把输出钉死。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提炼函数&lt;/strong&gt;：把 switch 算钱的部分提炼成 &lt;code&gt;amountFor(aPerformance)&lt;/code&gt;，把积分计算提炼成 &lt;code&gt;volumeCreditsFor()&lt;/code&gt;。每提炼一个，跑一次测试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提炼变量 + 改名&lt;/strong&gt;：&lt;code&gt;thisAmount&lt;/code&gt; 这种模糊名改成 &lt;code&gt;result&lt;/code&gt;，消除不必要的中间变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拆分阶段&lt;/strong&gt;：把“计算账单数据”和“渲染文本账单”拆成两个阶段，中间用一个数据结构传递。此时 &lt;code&gt;statement()&lt;/code&gt; 变成：&lt;code&gt;const data = createStatementData(invoice, plays); return renderPlainText(data);&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加功能&lt;/strong&gt;：此时写 &lt;code&gt;renderHtml(data)&lt;/code&gt; 只消费中间数据，计算逻辑一行不碰——HTML 版本轻松加上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;以多态取代条件表达式&lt;/strong&gt;：为“悲剧计算器”“喜剧计算器”建子类，switch 消失。加新戏种 = 加一个新子类。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;原始代码&amp;lt;br/&amp;gt;一个 40 行大函数&amp;lt;br/&amp;gt;无测试&amp;#34;] --&amp;gt; B[&amp;#34;补测试&amp;lt;br/&amp;gt;钉死现有行为&amp;#34;]
    B --&amp;gt; C[&amp;#34;提炼函数&amp;lt;br/&amp;gt;amountFor / volumeCreditsFor&amp;#34;]
    C --&amp;gt; D[&amp;#34;拆分阶段&amp;lt;br/&amp;gt;计算数据 vs 渲染文本&amp;#34;]
    D --&amp;gt; E[&amp;#34;中间数据结构&amp;lt;br/&amp;gt;statementData&amp;#34;]
    E --&amp;gt; F[&amp;#34;加功能：renderHtml&amp;lt;br/&amp;gt;复用同一份数据&amp;#34;]
    C --&amp;gt; G[&amp;#34;以多态取代条件表达式&amp;lt;br/&amp;gt;TragedyCalculator&amp;lt;br/&amp;gt;ComedyCalculator&amp;#34;]
    G --&amp;gt; H[&amp;#34;加功能：新戏种&amp;lt;br/&amp;gt;只需新增子类&amp;#34;]
    F --&amp;gt; I[&amp;#34;最终结构&amp;lt;br/&amp;gt;好改、好测、好扩展&amp;#34;]
    H --&amp;gt; I&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这个案例的精髓不在于手法本身（都不难），而在于&lt;strong&gt;节奏&lt;/strong&gt;：40 行的小函数，Fowler 也花了几十个小步、每步跑测试。他要演示的是：&lt;strong&gt;重构的安全性不来自你的技术，来自你的步幅&lt;/strong&gt;。步幅足够小，每一步都可以验证，出错最多浪费一步。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;5-重构与测试先补测试再动刀&#34;&gt;5. 重构与测试：先补测试，再动刀&lt;/h2&gt;
&lt;p&gt;第 2 版相对第 1 版（1999 年）最大的时代变化，就是自动化测试从“推荐”变成了“前提”。Fowler 的态度很明确：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;没有测试保护的重构不叫重构，叫“改代码并祈祷”。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;逻辑链条是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重构的定义是“不改变可观察行为”——那你怎么&lt;strong&gt;知道&lt;/strong&gt;行为没变？只能靠测试。&lt;/li&gt;
&lt;li&gt;重构要求小步快走——每一步都要验证，人工验证一遍要十分钟，自动化只要十秒。没有自动化，小步走不起来。&lt;/li&gt;
&lt;li&gt;面对祖传代码没有测试怎么办？&lt;strong&gt;先补测试再动刀&lt;/strong&gt;。补测试本身常常需要先轻量重构（比如提炼函数让依赖可以 mock），这就回到了两顶帽子：戴着测试帽做最小限度的结构调整，只为让测试能跑起来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Michael Feathers 在《修改代码的艺术》（Working Effectively with Legacy Code）里把“没有测试的代码”直接定义为遗留代码，和 Fowler 互为犄角。两本可以搭配读：Fowler 教你“怎么改”，Feathers 教你“改不动时怎么办”。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;要动一段代码&amp;#34;] --&amp;gt; B{&amp;#34;有测试覆盖吗？&amp;#34;}
    B -- &amp;#34;有&amp;#34; --&amp;gt; C[&amp;#34;跑一遍，确认全绿&amp;#34;]
    B -- &amp;#34;没有&amp;#34; --&amp;gt; D[&amp;#34;最小限度补测试&amp;lt;br/&amp;gt;（为可测性做的微调也算）&amp;#34;]
    D --&amp;gt; C
    C --&amp;gt; E[&amp;#34;做一小步重构&amp;#34;]
    E --&amp;gt; F{&amp;#34;测试全绿？&amp;#34;}
    F -- &amp;#34;绿了&amp;#34; --&amp;gt; G[&amp;#34;立即提交&amp;lt;br/&amp;gt;（小步提交 = 免费回滚点）&amp;#34;]
    F -- &amp;#34;红了&amp;#34; --&amp;gt; H[&amp;#34;撤销这一步&amp;lt;br/&amp;gt;或立刻修复，不要带着红灯继续&amp;#34;]
    G --&amp;gt; E
    H --&amp;gt; E
    E --&amp;gt; I{&amp;#34;目标达成？&amp;#34;}
    I -- &amp;#34;没有，继续小步&amp;#34; --&amp;gt; E
    I -- &amp;#34;达成&amp;#34; --&amp;gt; J[&amp;#34;收工：行为没变，结构更好&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这张“安全网流程图”里最反直觉也最重要的一条是：&lt;strong&gt;测试红了，默认动作是撤销这一步，而不是往前调&lt;/strong&gt;。小步走意味着你刚刚那一步的范围是已知的、可控的，退回上一步的代价几乎为零；带着红灯继续走，错误范围就不可控了。配合小步提交（每步都 commit），你就拥有了一条随时可以回滚的时间线。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;6-重构的时机与节奏融进日常而不是立项&#34;&gt;6. 重构的时机与节奏：融进日常，而不是立项&lt;/h2&gt;
&lt;h3 id=&#34;61-三次法则与顺手重构&#34;&gt;6.1 三次法则与“顺手重构”&lt;/h3&gt;
&lt;p&gt;Fowler 给的实用规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;事不过三，三则重构&lt;/strong&gt;（Rule of Three，源自 Don Roberts）：第一次写就写，第二次重复捏着鼻子忍，第三次必须提炼。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加功能前重构&lt;/strong&gt;：代码结构碍事，先重构到“加这个功能很容易”，再加功能（两顶帽子）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修 bug 时重构&lt;/strong&gt;：bug 之所以藏得深，往往因为代码难懂。把 bug 修掉的同时，把“让 bug 藏不住”的结构改出来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;code review 时重构&lt;/strong&gt;：看到别人的坏味道，当面演示一次提炼函数，比留十句评论有用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-破窗效应与童子军军规&#34;&gt;6.2 破窗效应与童子军军规&lt;/h3&gt;
&lt;p&gt;犯罪学里的“破窗效应”：一栋楼有几扇破窗没人修，很快所有窗户都会被砸——混乱会邀请更多混乱。代码库一模一样：第一处“先这样吧，回头再改”开了头，后面所有人都会照着最烂的那段代码的标准来写。&lt;/p&gt;
&lt;p&gt;对策是 Bob Martin 普及的&lt;strong&gt;童子军军规&lt;/strong&gt;（Boy Scout Rule）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;离开营地时，要让它比你来时更干净。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;落到代码上：每次提交，都顺手改进一点点——改一个烂名字、提炼一个长函数、删掉一段死代码。不需要翻天覆地，只要&lt;strong&gt;持续为正&lt;/strong&gt;。一个团队做到这点，代码质量会随时间收敛而不是发散。这是对付“祖传代码”唯一被验证过可持续的策略：不是一次性手术，是持续的免疫反应。&lt;/p&gt;
&lt;h3 id=&#34;63-什么时候不重构&#34;&gt;6.3 什么时候&lt;strong&gt;不&lt;/strong&gt;重构&lt;/h3&gt;
&lt;p&gt;书里同样重要的克制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代码又烂又永远不用改&lt;/strong&gt;——让它烂着，重构它没有回报。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重写比重构便宜&lt;/strong&gt;——偶尔成立，但门槛极高，见下节的 Netscape 教训。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;离截止日期太近且重构不能帮上眼前的忙&lt;/strong&gt;——重构的回报在下一次修改，先交付，记技术债，回头还。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;记住：&lt;strong&gt;重构是经济学决策&lt;/strong&gt;。每一步都应该能回答“这能让接下来的修改更便宜吗？”答不上来的重构是自我感动。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;7-大规模重写的教训netscape-之死&#34;&gt;7. 大规模重写的教训：Netscape 之死&lt;/h2&gt;
&lt;p&gt;聊到重构，绕不开软件工程史上最著名的一次“我们不重构了，我们重写”的惨案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;背景&lt;/strong&gt;：Netscape Navigator 曾统治浏览器市场，但代码库在快速迭代中积重难返。1997 年前后，Netscape 决定&lt;strong&gt;抛弃旧代码，从头重写浏览器引擎&lt;/strong&gt;（即后来的 Gecko / Mozilla 项目）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结果&lt;/strong&gt;：重写花了将近三年。这三年里 Netscape 几乎没有发布有竞争力的新版本——6.0 版本直到 2000 年底才问世。与此同时，微软的 IE 持续迭代、免费捆绑 Windows，市场份额被彻底翻盘。等 Mozilla 成品出来时，浏览器战争已经结束了。&lt;/p&gt;
&lt;p&gt;Joel Spolsky 在 2000 年那篇著名文章《Things You Should Never Do, Part I》里拿 Netscape 当反面教材，结论斩钉截铁：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重写是你能对软件做的最糟糕的战略性错误，没有之一。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;他的论证值得背下来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;旧代码不是垃圾，是知识的化石&lt;/strong&gt;。那些丑陋的补丁，每一个都对应一个真实用户踩过的坑、一个边界情况。重写时你看不见这些知识，于是用户会在新版本里重新踩一遍所有的坑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重写期间你无法交付&lt;/strong&gt;。老代码烂归烂，它在生产环境里跑着、赚着钱；新代码在成型之前一文不值。竞争对手不会等你。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重写通常伴随着“这次一定要设计完美”的野心&lt;/strong&gt;——这正是“夸夸其谈未来性”的企业级版本，工期必然失控。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fowler 在第 1 版就引用过这个故事来论证：&lt;strong&gt;重构的价值恰恰在于它是重写的替代品&lt;/strong&gt;。用持续的小步重构，让烂代码在“一直保持可交付”的状态下逐步变好，是风险唯一可控的路径。&lt;/p&gt;
&lt;p&gt;当然要有公允的注脚：后来 Mozilla 代码确实成了 Firefox 的底子，长期看那次重写并非全无产出。但&lt;strong&gt;作为商业决策，Netscape 公司没活到收获的那天&lt;/strong&gt;——这正是 Joel 论点最残酷的部分。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;8-带走的清单&#34;&gt;8. 带走的清单&lt;/h2&gt;
&lt;h3 id=&#34;81-核心心智模型五句话&#34;&gt;8.1 核心心智模型（五句话）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;重构 = &lt;strong&gt;不改变可观察行为&lt;/strong&gt;的结构调整；目标不是美，是&lt;strong&gt;让下次修改更便宜&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两顶帽子&lt;/strong&gt;：加功能和重构永不同时戴。&lt;/li&gt;
&lt;li&gt;坏味道清单 = 启发法清单：闻到味道 → 查名录 → 用对应手法。味道是提示，不是罪状。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;刷牙，不大扫除&lt;/strong&gt;：小步、频繁、顺手；童子军军规，每次提交留代码更干净一点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先补测试，再动刀&lt;/strong&gt;；测试红了先撤销，不带着红灯往前走。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;82-新人避坑指南&#34;&gt;8.2 新人避坑指南&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;坑一：把重构当借口拖延交付。&lt;/strong&gt; 重构是加功能的加速器不是挡箭牌。每次重构前问自己：这步能让手头这个需求更快落地吗？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;坑二：没有测试就动祖传代码。&lt;/strong&gt; 先补测试。补测试时发现代码不可测，先做最小限度的可测性重构（提炼函数、依赖注入），别一步到位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;坑三：PR 里混着“重构 + 新功能 + 修 bug”。&lt;/strong&gt; reviewer 无法区分行为变化来自哪里。拆 PR，或者至少拆 commit。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;坑四：看到 switch 就上多态，看到简单类就套设计模式。&lt;/strong&gt; 夸夸其谈未来性也是坏味道。YAGNI。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;坑五：迷信“等我有整块时间再重构”。&lt;/strong&gt; 那一天永远不会来。能融进日常的小重构才有复利，立项式重构大概率变成 Netscape 式豪赌。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;坑六：改名字嫌麻烦。&lt;/strong&gt; IDE 的安全重命名几秒钟一次，这是全软件业性价比最高的操作，没有之一。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;83-一页速查味道--手法&#34;&gt;8.3 一页速查：味道 → 手法&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;mindmap
  root((坏味道&amp;lt;br/&amp;gt;→&amp;lt;br/&amp;gt;重构手法))
    代码冗余
      重复代码 → 提炼函数
      过长函数 → 提炼函数
      过长参数列表 → 引入参数对象
      数据泥团 → 提炼类
      冗赘元素 → 内联函数
    职责错配
      依恋情结 → 搬移函数
      发散式变化 → 拆分阶段
      霰弹式修改 → 搬移函数聚拢
      内幕交易 → 搬移函数减少私聊
      过大的类 → 提炼类
    类型与状态
      基本类型偏执 → 以对象取代基本类型
      重复的switch → 以多态取代条件表达式
      全局数据 → 封装变量
      可变数据 → 拆分变量/值对象
      临时字段 → 提炼类
    意图不清
      神秘命名 → 改名
      注释 → 提炼函数让代码自解释
      复杂表达式 → 提炼变量
      消息链 → 隐藏委托关系
      中间人 → 移除中间人&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;9-写在最后&#34;&gt;9. 写在最后&lt;/h2&gt;
&lt;p&gt;《重构》这本书最被低估的一点：它表面上是一本“手法名录”，实际上是一套&lt;strong&gt;把代码质量从“良心活”变成“机械活”的方法论&lt;/strong&gt;。坏味道给你嗅觉，名录给你机械步骤，测试给你安全网，两顶帽子给你节奏——四样东西配齐，重构就从“需要勇气和天赋的大工程”变成了“不需要下决心就能做的日常小事”。&lt;/p&gt;
&lt;p&gt;而日常小事的复利是惊人的。每天让代码干净 1%，一年后它和自然腐化的代码库就是两个物种。这本书 1999 年出第 1 版、2018 年出第 2 版，横跨二十年依然是软件工程必读，原因无他：&lt;strong&gt;代码会腐化是永恒规律，而刷牙永远有效&lt;/strong&gt;。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>《修改代码的艺术》全书导读：遗留代码不是耻辱，是一张还没铺开的防护网</title>
      <link>https://ftmao.com/blog/bookstore/working-effectively-with-legacy-code-notes/</link>
      <pubDate>Sat, 08 Jul 2023 10:40:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/bookstore/working-effectively-with-legacy-code-notes/</guid>
      <description>&lt;h1 id=&#34;修改代码的艺术全书导读遗留代码不是耻辱是一张还没铺开的防护网&#34;&gt;《修改代码的艺术》全书导读：遗留代码不是耻辱，是一张还没铺开的防护网&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;参考书籍：Michael Feathers《修改代码的艺术》（&lt;em&gt;Working Effectively with Legacy Code&lt;/em&gt;，Prentice Hall 2004，中文版由人民邮电出版社/机械工业出版社先后引进）。
目标读者：没读过这本书、正在或即将维护&amp;quot;别人写的、不敢动的&amp;quot;代码库的程序员。
阅读目标：把全书最锋利的一个定义、一个模型（seam）、一个工作法（characterization test）、一组依赖解除技术讲清楚，最后编译成可执行的日常动作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;一这本书是什么一个定义就是全书最锋利的一刀&#34;&gt;一、这本书是什么：一个定义就是全书最锋利的一刀&lt;/h2&gt;
&lt;p&gt;这本 2004 年的书，副标题式的宣言只有一个：&lt;strong&gt;遗留代码（legacy code）就是没有测试的代码&lt;/strong&gt;（书中大意）。&lt;/p&gt;
&lt;p&gt;先把成书背景交代一句。2004 年，极限编程和 TDD 刚兴起不久，&amp;ldquo;写新代码时先写测试&amp;quot;的理念已经开始传播，但行业里沉默的大多数面对的现实是：&lt;strong&gt;手头维护的系统写出来的时候还没有单元测试这回事&lt;/strong&gt;。新书都在教&amp;quot;怎么从零写好代码&amp;rdquo;，几乎没人教&amp;quot;怎么面对已经写坏的代码&amp;quot;——而后者才是绝大多数程序员的日常。Feathers 这本书填的就是这个空档，书名里的 Working Effectively 不是修辞：它要解决的问题是&amp;quot;明天就要改这段代码，怎么办&amp;quot;，不是&amp;quot;理想情况下代码应该长什么样&amp;quot;。&lt;/p&gt;
&lt;p&gt;注意这个定义的刁钻之处。我们平常说&amp;quot;遗留代码&amp;quot;，指的是&amp;quot;前人留下的、老旧的、看不懂的代码&amp;quot;——一个时间属性。Feathers 把它换成了一个工程属性：&lt;strong&gt;没有测试覆盖的代码，哪怕昨天刚写完，也已经是遗留代码；有测试覆盖的老代码，反而不遗留。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一刀切下去，很多习以为常的现象立刻有了解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么你昨天写的代码今天就不敢改？因为它从出生起就没有测试，它就是遗留代码。&lt;/li&gt;
&lt;li&gt;为什么有的十年老系统改起来反而顺手？因为核心路径上有测试，行为是可见的。&lt;/li&gt;
&lt;li&gt;为什么&amp;quot;重写&amp;quot;的冲动总是错的？因为重写完的新代码依然没测试的话，你只是生产了一份新的遗留代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;书里还有一层更狠的推论（大意）：&lt;strong&gt;没有测试的代码就是坏代码&lt;/strong&gt;——不管它写得多漂亮。因为判断代码好坏的第一标准不是优雅，而是&amp;quot;能不能安全地改&amp;quot;，而安全的前提是你能看见改完之后行为有没有变。看不见行为，优雅就是一张裱在危房里的画。&lt;/p&gt;
&lt;p&gt;这和 &lt;a href=&#34;https://ftmao.com/blog/blog/bookstore/code-complete-learning-notes/&#34;&gt;《代码大全》笔记&lt;/a&gt; 里&amp;quot;软件的首要技术使命是管理复杂度&amp;quot;是同一件事的两面：复杂度管不住，表现就是行为不可观测；行为不可观测，改动就退化成赌博。&lt;/p&gt;
&lt;p&gt;这个定义还直接解开了遗留系统维护的头号死结——书中称之为一个两难困境（大意）：&lt;strong&gt;要安全地改代码，得先有测试；要给代码加测试，往往得先改代码&lt;/strong&gt;（把依赖解开才能放进测试夹具）。鸡生蛋蛋生鸡，多数人卡在这里就放弃了，于是继续 edit and pray。Feathers 给出的破法是这本书全部技术部分的主线：&lt;strong&gt;允许自己做&amp;quot;没有测试看护的最小改动&amp;quot;，但只限于一种改动——为了造出 seam 而做的依赖解除&lt;/strong&gt;。这类改动必须保守到近乎机械（提取一个方法、加一个参数，不碰任何逻辑），换回来的回报是：代码进了测试夹具，之后的一切修改都有了看护。先用最小风险换 leverage，再用 leverage 换安全——这是全书方法论的现金流结构。&lt;/p&gt;
&lt;p&gt;顺带一提这本书的体例：它不是一本&amp;quot;读完放书架&amp;quot;的书，而是一本&lt;strong&gt;工具手册&lt;/strong&gt;。前半本讲心法和工作流（怎么面对改不动的代码），后半本是一本&amp;quot;症状—处方&amp;quot;对照目录（&amp;ldquo;我改不动这个方法，因为它的依赖在构造函数里 new 出来&amp;rdquo;→ 查 Parameterize Constructor）。读法和读 &lt;a href=&#34;https://ftmao.com/blog/blog/posts/design-patterns-intro/&#34;&gt;《设计模式》入门&lt;/a&gt; 一样：第一遍通读建立索引，真正干活时按症状查表。&lt;/p&gt;
&lt;h2 id=&#34;二改代码的两种模式edit-and-pray-vs-cover-and-modify&#34;&gt;二、改代码的两种模式：edit and pray vs cover and modify&lt;/h2&gt;
&lt;p&gt;Feathers 把程序员修改代码的真实状态分成两种模式（书中大意）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 祈祷模式[&amp;#34;edit and pray&amp;lt;br/&amp;gt;编辑后祈祷&amp;#34;]
        A1[&amp;#34;读懂一部分代码&amp;#34;] --&amp;gt; A2[&amp;#34;凭感觉改&amp;#34;]
        A2 --&amp;gt; A3[&amp;#34;编译通过&amp;#34;]
        A3 --&amp;gt; A4[&amp;#34;上线前祈祷&amp;lt;br/&amp;gt;希望没碰坏别处&amp;#34;]
    end
    subgraph 覆盖模式[&amp;#34;cover and modify&amp;lt;br/&amp;gt;覆盖后修改&amp;#34;]
        B1[&amp;#34;找到改动点附近的 seam&amp;#34;] --&amp;gt; B2[&amp;#34;先写测试&amp;lt;br/&amp;gt;罩住现有行为&amp;#34;]
        B2 --&amp;gt; B3[&amp;#34;在测试看护下改代码&amp;#34;]
        B3 --&amp;gt; B4[&amp;#34;测试红了就停&amp;lt;br/&amp;gt;绿了就继续&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;两种模式的差别不在技术，在&lt;strong&gt;恐惧的分配方式&lt;/strong&gt;。edit and pray 把恐惧攒到最后一刻一次性兑现——祈祷上线别出事；cover and modify 把恐惧拆成无数次测试运行，每次只消化一小口。&lt;/p&gt;
&lt;p&gt;所以这本书对测试的定位和教科书完全不同：&lt;strong&gt;测试在这里不是 QA 工具，不是质量部门的产出物，而是改动代码的安全网&lt;/strong&gt;。它的价值排序是——先让&amp;quot;改&amp;quot;这个动作变得不恐怖，质量提升是顺带的收益。一个从没写过单元测试的团队，第一本要补的不是&amp;quot;测试覆盖率指标&amp;quot;，而是&amp;quot;下一次改动之前先罩一层网&amp;quot;的习惯。&lt;/p&gt;
&lt;p&gt;书中有句话的意思值得抄在工位上（大意）：我们之所以要在修改前写测试，是因为&lt;strong&gt;在坏代码里，修改带来的伤害往往是延迟发作的&lt;/strong&gt;——你以为只动了 A，其实 B 的行为悄悄变了，三周后才在客户现场爆出来。测试网的作用就是把&amp;quot;三周后&amp;quot;压缩成&amp;quot;三秒后&amp;quot;。&lt;/p&gt;
&lt;p&gt;既然 cover and modify 明显更好，为什么大多数团队还是常年泡在 edit and pray 里？书里给出的解释很实际（大意）：&lt;strong&gt;两种模式的成本结算周期不一样&lt;/strong&gt;。edit and pray 的成本是赊账——改动当下很快，代价在未来某次线上事故里一次性扣掉，而且常常由别人买单；cover and modify 的成本是现付——写测试、解依赖的功夫当下就要掏，收益却摊在之后每一次改动里慢慢回来。人都是短视的，团队也是，所以破局的钥匙不是讲道理，而是&lt;strong&gt;把第一次&amp;quot;现付&amp;quot;的门槛降到足够低&lt;/strong&gt;——这正是后半本依赖解除技术目录存在的意义：它们把&amp;quot;给这段代码加第一个测试&amp;quot;从一天的工程压缩到一小时的手术。&lt;/p&gt;
&lt;p&gt;还有一点容易被误读：这本书讲的测试和 TDD 不是一回事，也不冲突。TDD 是&amp;quot;先写测试驱动出新代码的设计&amp;quot;，往前看；这本书的 characterization test 是&amp;quot;先写测试固化住旧代码的行为&amp;quot;，往后看。一个管新建，一个管存量。&lt;strong&gt;遗留系统里正确的姿势是两条腿走路：存量行为用 characterization test 罩住，新增逻辑用 TDD 长出来&lt;/strong&gt;——两头往中间会师，系统的可测试面积只会越来越大。&lt;/p&gt;
&lt;h2 id=&#34;三seam-模型全书的技术核心&#34;&gt;三、seam 模型：全书的技术核心&lt;/h2&gt;
&lt;p&gt;如果这本书只带走一个词，那就是 &lt;strong&gt;seam（接缝）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;定义（书中大意）：&lt;strong&gt;seam 是程序中这样一个位置——你不需要修改这段代码本身，就能改变它的行为。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为什么这个概念是技术核心？因为改遗留代码的全部困难可以归结成一句话：&lt;strong&gt;代码里塞满了硬编码的依赖，你没法把它放进一个测试夹具（test harness）里单独观察。&lt;/strong&gt; 而 seam 就是你在依赖丛林里找到的下刀点——庖丁解牛，顺着纹理下刀，牛的筋骨自然分开；seam 就是代码的纹理。&lt;/p&gt;
&lt;p&gt;Feathers 列出三类 seam（大意）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    S[&amp;#34;seam&amp;lt;br/&amp;gt;不改代码就能改变行为的点&amp;#34;] --&amp;gt; O[&amp;#34;对象接缝&amp;lt;br/&amp;gt;Object Seam&amp;#34;]
    S --&amp;gt; P[&amp;#34;预处理接缝&amp;lt;br/&amp;gt;Preprocessing Seam&amp;#34;]
    S --&amp;gt; L[&amp;#34;链接接缝&amp;lt;br/&amp;gt;Linking Seam&amp;#34;]
    O --&amp;gt; O1[&amp;#34;例子：把 new 出来的依赖&amp;lt;br/&amp;gt;换成通过参数/子类注入的替身&amp;#34;]
    O --&amp;gt; O2[&amp;#34;适用：Java / C# 等&amp;lt;br/&amp;gt;面向对象语言的主战场&amp;#34;]
    P --&amp;gt; P1[&amp;#34;例子：C/C++ 用宏和条件编译&amp;lt;br/&amp;gt;在测试构建里替换掉某个调用&amp;#34;]
    P --&amp;gt; P2[&amp;#34;适用：有预处理器的语言&amp;lt;br/&amp;gt;测试代码和生产代码两份编译&amp;#34;]
    L --&amp;gt; L1[&amp;#34;例子：换掉链接的库&amp;lt;br/&amp;gt;测试时链接桩版本而不是真库&amp;#34;]
    L --&amp;gt; L2[&amp;#34;适用：编译型语言&amp;lt;br/&amp;gt;把依赖推迟到链接期&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三种 seam 背后的原理是同一个：&lt;strong&gt;程序的行为由很多个&amp;quot;点&amp;quot;共同决定——调用点、编译点、链接点、对象创建点。每多一个这样的点，就多一次不动手术也能换行为的机会。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;三类 seam 各自的使用场景值得多说一句：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对象 seam&lt;/strong&gt; 是日常用得最多的。书中反复强调的一个手法（大意）：当一个类在构造函数里 &lt;code&gt;new&lt;/code&gt; 出一个绕不开的依赖（数据库连接、硬件接口、全局单例），你无法实例化它去测试——这时别急着重写，先看能不能造一个 seam：把依赖变成参数传进来，或者子类化后覆写那个获取依赖的方法。&lt;strong&gt;seam 的目标不是好设计，是 leverage（杠杆点）——一个能让你把这块代码抬进测试夹具的支点。&lt;/strong&gt; 设计可以之后再收拾，先拿到支点再说。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预处理 seam&lt;/strong&gt; 是 C/C++ 程序员的专属武器（大意）：用宏和条件编译，让测试构建里的某个函数调用指向桩实现，生产构建里指向真实实现——同一份源码，两种行为，而源码本身一行没改。它也能在 Java 等语言里以&amp;quot;测试用构建脚本替换类路径&amp;quot;的形式变相存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链接 seam&lt;/strong&gt; 更偏构建层（大意）：把依赖推迟到链接期决定——测试时链接一个桩版本的库，生产时链接真库。它在遗留 C 系统里常常是把整段代码撬进测试夹具的唯一入口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;理解 seam 之后回头看 &lt;a href=&#34;https://ftmao.com/blog/blog/posts/design-patterns-intro/&#34;&gt;《设计模式》入门笔记&lt;/a&gt; 会有新感觉：工厂、策略、依赖注入这些模式，本质都是在代码里预先埋好的 seam。遗留代码的悲剧不是没有用模式，而是当年没人在乎缝留在哪里。&lt;/p&gt;
&lt;p&gt;书里还给 seam 配了一个搭档概念——&lt;strong&gt;enabling point（使能点，大意）&lt;/strong&gt;：seam 是&amp;quot;行为可以改变的位置&amp;quot;，enabling point 是&amp;quot;你在哪儿做出那个选择&amp;quot;。同一个 seam 可能有多个使能点：一个方法调用是 seam，而选择传哪个对象的使能点，可能在调用处、在构造函数里、在配置文件里。&lt;strong&gt;找 seam 的完整动作是两步：先定位&amp;quot;哪里能换行为&amp;quot;，再决定&amp;quot;在哪儿换最顺手&amp;quot;。&lt;/strong&gt; 很多改不动代码的挫败感，其实来自只做了第一步就动手——缝找到了，但选错了下针的位置。&lt;/p&gt;
&lt;h2 id=&#34;四characterization-test先固化行为再动代码&#34;&gt;四、characterization test：先固化行为，再动代码&lt;/h2&gt;
&lt;p&gt;有了 seam，接下来的问题是：测试写什么？这是全书反直觉的地方。&lt;/p&gt;
&lt;p&gt;教科书说测试是&amp;quot;验证代码的正确性&amp;quot;。Feathers 说不对，在遗留代码场景里你要写的是 &lt;strong&gt;characterization test（特征测试/刻画测试）&lt;/strong&gt;——&lt;strong&gt;它不验证代码&amp;quot;对不对&amp;quot;，它只固化代码&amp;quot;现在是什么样&amp;quot;&lt;/strong&gt;（大意）。&lt;/p&gt;
&lt;p&gt;区别很关键。遗留代码里充满了&amp;quot;看起来是 bug 但业务已经依赖了十年&amp;quot;的行为。你没法站在上帝视角判定哪个行为是&amp;quot;正确&amp;quot;的——你能做的、必须做的，是先把当前行为原样钉在测试里，让任何意外的漂移立刻报警。正确性可以之后讨论，&lt;strong&gt;先保证&amp;quot;变化&amp;quot;这件事是可观测的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个定位和传统的回归测试也不一样。回归测试隐含着&amp;quot;我们曾经知道什么是对的，现在防止它退化&amp;quot;；而面对一段没人说得清意图的遗留代码，你连&amp;quot;对&amp;quot;的基线都没有（大意）。characterization test 的诚实之处在于它承认这一点：&lt;strong&gt;我不知道这段代码做得对不对，但我知道它现在做成什么样，我要保证的是&amp;quot;我改完之后它还是这样&amp;quot;——或者当它必须变的时候，变的那一处是我有意为之的。&lt;/strong&gt; 把&amp;quot;无意的变化&amp;quot;降为零，把&amp;quot;有意的变化&amp;quot;收窄到一处，这就是在遗留代码上干活的全部安全学。&lt;/p&gt;
&lt;p&gt;一个常见疑问：特征测试要写多少才够？书中的回答务实得让人安心（大意）：&lt;strong&gt;不需要覆盖整个系统，只需要覆盖你这次改动的影响面&lt;/strong&gt;。用影响草图圈出&amp;quot;改动会波及哪些行为&amp;quot;，把这个圈里的行为钉住就够了。圈外的代码这次没动，它的行为本来就变不了——真正的风险从来只在影响面之内。这也意味着特征测试是按需生长的：每改一次，网就密一格，从不做&amp;quot;覆盖率大跃进&amp;quot;。&lt;/p&gt;
&lt;p&gt;完整工作法四步：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;① 选 seam&amp;lt;br/&amp;gt;找到改动点附近&amp;lt;br/&amp;gt;能下刀的接缝&amp;#34;] --&amp;gt; B[&amp;#34;② 写观测测试&amp;lt;br/&amp;gt;断言传一个值&amp;lt;br/&amp;gt;跑出什么就钉住什么&amp;#34;]
    B --&amp;gt; C[&amp;#34;③ 改代码&amp;lt;br/&amp;gt;在测试看护下&amp;lt;br/&amp;gt;做最小修改&amp;#34;]
    C --&amp;gt; D[&amp;#34;④ 重构&amp;lt;br/&amp;gt;测试全绿的前提下&amp;lt;br/&amp;gt;顺手收拾结构&amp;#34;]
    D -.-&amp;gt;|&amp;#34;下次改动从这里继续&amp;#34;| A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;第二步有个很实用的细节（书中大意）：写特征测试时，&lt;strong&gt;断言先写一个你知道多半会失败的值，让测试跑一次，把真实输出抄进断言&lt;/strong&gt;。这不是作弊——你的目的不是证明代码正确，而是用测试当探针，观测代码此刻的行为，然后把观测结果固化下来。测试从&amp;quot;裁判&amp;quot;暂时变成&amp;quot;记录仪&amp;quot;，等它记录够了，才恢复裁判的身份。&lt;/p&gt;
&lt;p&gt;第四步的顺序同样不能乱：&lt;strong&gt;先改出功能、绿了，再重构&lt;/strong&gt;。重构和功能修改混在一次提交里，是遗留代码灾难的头号来源——测试红了，你分不清是功能改错了还是重构挪坏了。一次只动一种东西。&lt;/p&gt;
&lt;p&gt;还有一个贯穿全书的概念，解释了为什么&amp;quot;解依赖&amp;quot;这件事值得花那么大代价（大意）：把代码放进测试夹具，是为了同时获得两种能力——&lt;strong&gt;sensing（感知）和 separation（分离）&lt;/strong&gt;。感知，是你能观测到这段代码的行为，改了之后立刻知道结果变没变；分离，是你能把这段代码从整个系统里拆出来单独跑，不必拖起数据库、硬件、整个应用。很多依赖解除技术表面上是&amp;quot;为了让测试能跑&amp;quot;，实际上是在买这两样东西——sensing 买的是&amp;quot;看得见的胆子&amp;quot;，separation 买的是&amp;quot;快&amp;quot;。一个要跑三分钟的测试你不会常跑，一个不常跑的测试等于没有；所以书中对测试速度的要求近乎苛刻（大意）：&lt;strong&gt;单元测试必须快，慢测试会杀死整个反馈回路&lt;/strong&gt;——哪怕为此要解掉更多依赖。&lt;/p&gt;
&lt;h2 id=&#34;五依赖解除技术族八把常用的小刀&#34;&gt;五、依赖解除技术族：八把常用的小刀&lt;/h2&gt;
&lt;p&gt;全书后半本是一个技术目录，解决同一个问题：&lt;strong&gt;类和方法被依赖焊死了，放不进测试夹具，怎么办&lt;/strong&gt;。挑最常用、回报最快的八个讲，每个都是&amp;quot;一句话场景 + 一句话做法&amp;quot;：&lt;/p&gt;
&lt;p&gt;用之前先建立一个总认知（大意）：&lt;strong&gt;这族技术全部是&amp;quot;保守手术&amp;quot;&lt;/strong&gt;——它们中的大多数可以在没有测试看护的情况下做，因为每一步都机械到几乎不可能改变行为（提取一段代码、加一个参数、改一个可见性）。这不是说它们没有风险，而是说风险被压缩到了&amp;quot;编译器能替你检查&amp;quot;的量级。这是 Feathers 解开第一节那个两难困境的实际手法：用近乎无风险的微改动撬动 seam，seam 撬动测试，测试撬动之后一切真正的改动。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sprout Method（新生方法）&lt;/strong&gt;：要往一个没法测试的老方法里加逻辑。做法：新逻辑写成一个独立的新方法，老方法里只加一行调用——新方法可以独立测试，老方法只承担一行的风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sprout Class（新生类）&lt;/strong&gt;：要加的逻辑需要一堆新状态，塞不进老类。做法：新逻辑整体放进一个新类，老类只持有它的引用并在必要时调用——新类从第一天起就有测试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrap Method（包裹方法）&lt;/strong&gt;：要在老方法每次执行前后加点行为（记日志、发通知），但老方法不能动。做法：把老方法改名藏起来，用原方法名建一个新方法，先做新行为再调旧实现——调用方无感知。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrap Class（包裹类）&lt;/strong&gt;：要给整个类的行为外加一层职责（装饰器思路）。做法：新类持有一个老类实例，对外暴露同样的接口，附加行为都在新类里——老类一行不改。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extract Interface（提取接口）&lt;/strong&gt;：依赖是个具体大类，实例化就要拖起半个系统。做法：把被调用的那几个方法提取成接口，测试里传一个假的实现——接缝造在类型层面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extract and Override（提取并覆写）&lt;/strong&gt;：方法中间嵌着一个没法在测试里执行的调用（读硬件、发网络请求）。做法：把那一小段提取成受保护的方法，测试里子类化并覆写成空实现或桩——最小的手术换最大的可测性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subclass and Override（子类化并覆写）&lt;/strong&gt;：整个类的构造函数或关键流程里有绕不开的硬依赖。做法：测试里建一个继承它的测试子类，覆写掉获取依赖的方法——不改动生产代码就撬开了行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parameterize Constructor / Method（参数化构造函数/方法）&lt;/strong&gt;：依赖是在内部 &lt;code&gt;new&lt;/code&gt; 出来的，外部完全够不着。做法：把 &lt;code&gt;new&lt;/code&gt; 挪到构造参数或方法参数上，生产代码照常传真实对象，测试传假对象——这是把隐式依赖变显式的标准动作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这八把刀的共同哲学值得记住：&lt;strong&gt;它们的直接目的都不是&amp;quot;让设计更好&amp;quot;，而是&amp;quot;让代码能被测试&amp;quot;&lt;/strong&gt;。Feathers 把这两件事严格分开（大意）：解依赖是为了找到 leverage，一旦测试夹具立住了，设计的债可以在测试看护下慢慢还。反过来，没有测试就奔着&amp;quot;好设计&amp;quot;去大刀阔斧，那是高级版的 edit and pray。&lt;/p&gt;
&lt;p&gt;这一族技术和 &lt;a href=&#34;https://ftmao.com/blog/blog/bookstore/clean-architecture-book-notes/&#34;&gt;《整洁架构》笔记&lt;/a&gt; 里的依赖规则正好互为镜像：整洁架构讲的是&amp;quot;从第一天起就让依赖指向正确方向&amp;quot;，这本书讲的是&amp;quot;方向已经错了十年，怎么一格一格扳回来&amp;quot;。前者是规划新城，后者是老城改造，工具箱不同，底层都是同一套 seam 思维。&lt;/p&gt;
&lt;p&gt;实战中怎么选刀？有一个朴素的排序原则：&lt;strong&gt;能 Sprout 就不 Wrap，能 Wrap 就不动老代码内部，万不得已才进老代码里做 Extract / Parameterize&lt;/strong&gt;。手术越小，没有测试看护的那一段就越短，风险就越可控。另外书里的技术目录比这八把刀长得多（大意）——还有把参数降为更基础类型的 Primitivize Parameter、把一组方法整体提到抽象基类的 Pull Up Feature 等等——症状不同处方不同，真到动手时值得按&amp;quot;症状&amp;quot;去翻原书后半本的对照目录，这正是它作为工具手册的用法。&lt;/p&gt;
&lt;h2 id=&#34;六巨型类怎么拆一只上千行的怪兽&#34;&gt;六、巨型类：怎么拆一只上千行的怪兽&lt;/h2&gt;
&lt;p&gt;遗留系统里几乎必有一只巨型类——上千行、几十个方法、什么都管。Feathers 给的拆法（大意）分三步：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：职责识别。&lt;/strong&gt; 先别拆，先读。用笨办法给方法分组：把方法按&amp;quot;它们共同操作哪些字段、共同服务哪类调用方&amp;quot;聚类；给私有方法起更长的描述性名字，看名字里反复出现的主题；把类&amp;quot;为谁工作&amp;quot;的所有角色列出来。一个类里往往能数出三五个本可以独立存在的职责——这就是 &lt;a href=&#34;https://ftmao.com/blog/blog/bookstore/code-complete-learning-notes/&#34;&gt;《代码大全》&lt;/a&gt; 说的单一职责，只是这里你是逆着历史考古。&lt;/p&gt;
&lt;p&gt;这一步有几个书中的实用技巧（大意）：一是&lt;strong&gt;看私有方法&lt;/strong&gt;——巨型类里的私有方法往往暗示着一个可以独立出去的职责，因为它们本来就是&amp;quot;类内部的内部&amp;quot;；二是&lt;strong&gt;数调用方&lt;/strong&gt;——如果类的不同方法群服务的是完全不同的调用方（报表模块用这三个方法，同步模块用那五个方法），调用方就是职责的天然分界线；三是&lt;strong&gt;问&amp;quot;这个类在为谁做决定&amp;quot;&lt;/strong&gt;——决策点聚集的地方就是职责所在。这些技巧的共同点是：不依赖你对业务的先验理解，从代码自身的结构里把职责挖出来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：责任分组。&lt;/strong&gt; 把识别出的职责画成草图：哪个方法群服务于哪个职责，哪些方法脚踩两只船。脚踩两只船的方法是拆分的难点，也是拆分的钥匙——它们往往揭示了两个职责之间真正的边界。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步：挤压边界。&lt;/strong&gt; 不是一次性把大类切成五个小类（那是 edit and pray 的豪华版），而是&lt;strong&gt;每次只把一个职责里的一小部分挪出去&lt;/strong&gt;——用 Sprout Class 或提取类的方式，挪一小块，测试看护下跑绿，再挪一小块。原类的边界被一圈一圈挤压收缩，像退潮一样。挤压策略的精髓是：&lt;strong&gt;任何时刻系统都是可运行的，任何一步都是可回退的。&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    G[&amp;#34;巨型类&amp;lt;br/&amp;gt;千行·几十个方法&amp;lt;br/&amp;gt;什么职责都有&amp;#34;] --&amp;gt; S1[&amp;#34;识别职责&amp;lt;br/&amp;gt;方法分组&amp;lt;br/&amp;gt;数出 3~5 个隐藏角色&amp;#34;]
    S1 --&amp;gt; S2[&amp;#34;画责任草图&amp;lt;br/&amp;gt;找到脚踩两只船的方法&amp;lt;br/&amp;gt;那才是真正的边界&amp;#34;]
    S2 --&amp;gt; S3[&amp;#34;每次挪一小块&amp;lt;br/&amp;gt;Sprout Class / 提取类&amp;lt;br/&amp;gt;测试看护下跑绿&amp;#34;]
    S3 --&amp;gt; S4[&amp;#34;边界一圈圈收缩&amp;lt;br/&amp;gt;系统时刻可运行&amp;lt;br/&amp;gt;每步可回退&amp;#34;]
    S4 -.-&amp;gt;|&amp;#34;半年后怪兽小一半&amp;#34;| S3&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;配套的两个心法（大意）：一是拆巨型类时别追求完美边界，先追求&amp;quot;可测试的边界&amp;quot;——边界丑一点没关系，测试网立住了边界可以随时再调；二是拆分过程中行为必须逐字节保持，这是 characterization test 的主场。&lt;/p&gt;
&lt;p&gt;还有一个心理层面的提醒（书中大意）：&lt;strong&gt;拆巨型类的进度要用&amp;quot;季度&amp;quot;来度量，不要用&amp;quot;冲刺&amp;quot;来度量。&lt;/strong&gt; 一个上千行的类是十年业务沉积的结果，指望一个迭代拆完，结局往往是拆到一半需求来了，留下一个比原来更糟的半拆状态。正确的节奏是&amp;quot;每次顺手&amp;quot;：每次因业务需求碰这个类，就顺手把一个职责的一小块挪出去。半年后回头看，怪兽已经小了一半——而没有任何一个迭代被&amp;quot;重构专项&amp;quot;耽误过。&lt;/p&gt;
&lt;h2 id=&#34;七改不动的时候先理解再下刀&#34;&gt;七、改不动的时候：先理解，再下刀&lt;/h2&gt;
&lt;p&gt;书里专门处理了&amp;quot;代码我完全看不懂，但明天必须改&amp;quot;的绝境。在进工具箱之前先记住一个前提（大意）：&lt;strong&gt;&amp;ldquo;改不动&amp;quot;往往不只是代码问题，还是反馈回路问题。&lt;/strong&gt; 如果构建一次要半小时、启动一次要拖起三个中间件，那再好的 seam 和测试也救不了你——反馈慢到一定程度，人就会退回 edit and pray，因为&amp;quot;跑一遍验证&amp;quot;的成本高到不划算。所以书里把&amp;quot;让代码能快速进测试夹具跑起来&amp;quot;当作和&amp;quot;解依赖&amp;quot;同等重要的事：测试夹具的意义就在于绕开整个系统的启动成本，让你在秒级的时间里看到行为变化。工具箱如下（大意）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;画草图，不写文档。&lt;/strong&gt; 读不懂就画：effect sketch（影响草图）——从你要改的方法出发，画出它影响谁、谁影响它，一张图看清代码的&amp;quot;引力范围&amp;rdquo;；feature sketch（特征草图）——按职责给方法分组。草图的价值不在图本身，在于画的过程中你被迫建立了对代码的心智模型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在代码上做标注。&lt;/strong&gt; 临时把大方法里的段落抽出来起个名（不提交也行），或者用注释标出&amp;quot;这一段在干嘛&amp;quot;。理解会欺骗记忆，标注不会。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;scratch refactoring（草稿式重构）&lt;/strong&gt;：一条很反直觉的建议——纯粹为了理解代码，可以随手重构它（抽方法、改名、挪位置），&lt;strong&gt;改完扔掉，不提交&lt;/strong&gt;。重构是理解代码最快的探针，比干读效率高得多；扔掉是为了不背负&amp;quot;改坏了&amp;quot;的风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删掉无用代码。&lt;/strong&gt; 死代码是最大的噪音源。确认没被调用的代码就删——版本控制里它永远活着，工作区里它只会碍眼。留下来的每一行都应该是有行为意义的行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新代码用 TDD。&lt;/strong&gt; 遗留代码里没有测试不是你不写测试的理由。往遗留系统里加新逻辑时，能用 Sprout 就用 Sprout，新方法新类从第一行起就 TDD——让新长出来的部分永远是&amp;quot;非遗留&amp;quot;的，系统的测试覆盖率会随每次改动自然上涨。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小步快走。&lt;/strong&gt; 每一步改动之间跑一次测试，绿了就存盘。步子小到&amp;quot;这一步不可能错&amp;quot;的程度，速度反而最快——因为省去了所有&amp;quot;改了一下午发现早就坏了、回滚到不知道哪个版本&amp;quot;的灾难恢复时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套&amp;quot;先理解&amp;quot;的策略和 &lt;a href=&#34;https://ftmao.com/blog/blog/bookstore/code-complete-learning-notes/&#34;&gt;《代码大全》学习笔记&lt;/a&gt; 里的调试心法同源：&lt;strong&gt;理解是修改的前置条件，而理解本身是可以工程化的&lt;/strong&gt;——草图、标注、scratch refactoring，都是把&amp;quot;读代码&amp;quot;从玄学变成动作的工具。&lt;/p&gt;
&lt;p&gt;这个工具箱里我最想单独强调 scratch refactoring，因为它违反了多数人的直觉：花两小时重构一份代码然后全部扔掉，听起来是纯浪费。但它的账要这么算——你需要的从来不是&amp;quot;重构的结果&amp;quot;，而是&amp;quot;重构过程中被迫看清的结构&amp;quot;。干读代码是被动接收，读三小时可能还是一团雾；动手重构是主动解剖，抽第三个方法的时候你就已经知道这段代码的骨架长什么样了。&lt;strong&gt;理解是这次任务的真实交付物之一&lt;/strong&gt;，代码只是理解的副产品——既然是副产品，扔掉并不可惜，版本库里那份没动过的才是资产。这和写文档之前先画草图、做方案之前先写 spike 是同一个道理：用小成本的可丢弃物，换大决策的确定性。&lt;/p&gt;
&lt;h2 id=&#34;八全书心法遗留代码是资产恐惧来自看不见&#34;&gt;八、全书心法：遗留代码是资产，恐惧来自看不见&lt;/h2&gt;
&lt;p&gt;读到最后会发现，这本技术书的底层是一篇心理学论文。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;遗留代码不是耻辱，是资产。&lt;/strong&gt; 它丑、它乱、它没人敢碰——但它在生产环境跑了十年，每一行丑陋的代码背后都可能固化着一次真实业务的教训、一个已经交付的需求、一个被客户依赖的行为。重写派看见的是债，Feathers 看见的是&lt;strong&gt;已经兑付过的价值&lt;/strong&gt;。正确的心态不是&amp;quot;这破代码该推倒&amp;quot;，而是&amp;quot;这份资产缺一张安全网，我来给它铺上&amp;quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;恐惧的来源是看不见行为。&lt;/strong&gt; 程序员面对遗留代码的恐惧，本质上和走夜路的恐惧同构——不是路难走，是看不见。characterization test 做的事情，就是把行为从&amp;quot;隐式的、存在于某次生产事故记忆里的&amp;quot;变成&amp;quot;显式的、写在断言里的&amp;quot;。行为一旦可见，恐惧就有了边界，改动就有了底气。这和我在方法论里反复说的&amp;quot;写下来&amp;quot;是同构的：&lt;strong&gt;不可见的东西统治你，可见的东西服务你。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;整本书的刀法，用庖丁解牛来对照最贴切：庖丁十九年解数千牛而刀刃若新发于硎，靠的不是力气，是&amp;quot;以无厚入有间&amp;quot;——找到牛体的天然纹理，顺着纹理下刀。&lt;strong&gt;seam 就是代码的纹理，characterization test 就是下刀前先摸清纹理的那只手。&lt;/strong&gt; 对遗留代码使蛮力的人，三年换一把刀；会找 seam 的人，刀越用越快。&lt;/p&gt;
&lt;p&gt;还有一层书里没有大声说、但全书处处都在暗示的心法：&lt;strong&gt;在遗留系统里，第一个写测试的人改变的不是代码，是团队的物理环境。&lt;/strong&gt; 一张测试网一旦存在，后来者改代码的默认姿势就变了——从&amp;quot;祈祷&amp;quot;变成&amp;quot;跑一遍看看&amp;quot;。安全网和恐惧一样会传染，方向相反而已。所以不要等&amp;quot;团队有了测试文化&amp;quot;才开始写测试，顺序是反的：你写下的第一个 characterization test，就是测试文化的第一个细胞。这也是&amp;quot;资产&amp;quot;视角的推论——测试网是极少数&lt;strong&gt;只涨不跌&lt;/strong&gt;的工程资产：它一旦固化住一个行为，就永远为之后所有的改动站岗，而且每新增一条断言，存量价值的保护范围就扩大一格。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    F[&amp;#34;面对遗留代码的恐惧&amp;lt;br/&amp;gt;看不见行为，不敢改&amp;#34;] --&amp;gt; T[&amp;#34;characterization test&amp;lt;br/&amp;gt;固化当前行为&amp;lt;br/&amp;gt;让不可见变可见&amp;#34;]
    T --&amp;gt; S[&amp;#34;seam&amp;lt;br/&amp;gt;找到下刀的纹理&amp;lt;br/&amp;gt;获得 leverage&amp;#34;]
    S --&amp;gt; D[&amp;#34;依赖解除技术族&amp;lt;br/&amp;gt;Sprout / Wrap / Extract&amp;lt;br/&amp;gt;Subclass / Parameterize&amp;#34;]
    D --&amp;gt; R[&amp;#34;在测试看护下&amp;lt;br/&amp;gt;改代码 + 挤压边界&amp;#34;]
    R --&amp;gt; C[&amp;#34;遗留代码逐步变成&amp;lt;br/&amp;gt;有测试覆盖的资产&amp;#34;]
    C -.-&amp;gt;|&amp;#34;恐惧被切成小份消化&amp;#34;| F&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;九这本书值不值得读&#34;&gt;九、这本书值不值得读&lt;/h2&gt;
&lt;p&gt;把全书压成一张图，就是&amp;quot;定义 → 模型 → 工作法 → 工具箱&amp;quot;四层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一个定义&lt;/strong&gt;：遗留代码 = 没有测试的代码。它把&amp;quot;遗留&amp;quot;从时间属性换成工程属性，顺带解释了为什么新写的代码也会一夜之间变遗留。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个模型&lt;/strong&gt;：seam。不改代码就能改变行为的点，是一切依赖解除技术的解剖学基础。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个工作法&lt;/strong&gt;：characterization test 四步——选 seam、写观测测试、改代码、重构。把&amp;quot;修改遗留代码&amp;quot;从赌博变成流程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个工具箱&lt;/strong&gt;：Sprout / Wrap / Extract / Subclass / Parameterize 一族依赖解除技术，按症状查表使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;优点很明确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题定义得准&lt;/strong&gt;。二十多年了，&amp;ldquo;遗留代码 = 没有测试的代码&amp;quot;依然是这个领域最锋利的一刀，没有之一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;技术可执行&lt;/strong&gt;。后半本的症状—处方目录不是空谈原则，每一条都能当天上手——这在软件工程书里并不多见。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;心态建设到位&lt;/strong&gt;。它把&amp;quot;面对烂代码的羞耻和恐惧&amp;quot;也当成工程问题处理，给出的解法是&amp;quot;让行为可见&amp;rdquo;，而不是&amp;quot;你要勇敢&amp;quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;局限也要说清楚：成书于 2004 年，例子语言以 Java/C++/C# 为主，链接 seam、预处理 seam 这些章节对纯动态语言程序员参考价值有限；书中还没有依赖注入容器、mock 框架、容器化测试环境这些今天的标配，很多当年需要手工解的依赖，现在框架已经替你解了。但它的底层模型——seam、leverage、sensing 与 separation——不过时，因为&lt;strong&gt;依赖问题不过时&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;推荐给两类人：正在维护没测试的老系统的工程师（直接读全书，后半本放案头当手册）；写代码从不写测试、想建立测试习惯的工程师（重点读前半本，先理解&amp;quot;测试是安全网&amp;quot;这个定位再谈技术）。&lt;/p&gt;
&lt;p&gt;阅读顺序上给一个实操建议：第一遍只读前半本，读到 characterization test 四步为止就停手去干活——找一个你正在维护的模块，完整地走一遍&amp;quot;找 seam → 写特征测试 → 在测试看护下改&amp;quot;，走完一遍再回来翻后半本的技术目录。这本书的全部价值都在&amp;quot;用&amp;quot;里，纯读完不用，三个月后只剩&amp;quot;遗留代码 = 没测试的代码&amp;quot;这一句口号。&lt;/p&gt;
&lt;h2 id=&#34;落地编译成工程师的日常动作&#34;&gt;落地：编译成工程师的日常动作&lt;/h2&gt;
&lt;p&gt;把这本书的方法论编译成日常动作，不用立志，只要执行五条。这五条的共同点是&lt;strong&gt;都不需要任何立项、任何批准、任何&amp;quot;重构专项&amp;quot;&lt;/strong&gt;——它们全部可以藏在日常的需求开发里完成，这正是 Feathers 式改良主义的精髓：不跟管理层要时间还债，而是让每次还债都搭在真实需求的便车上。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;接遗留系统的第一件事不是改代码，是找 seam、写特征测试。&lt;/strong&gt; 接到任何维护型任务，先把&amp;quot;我要改的地方能不能放进测试夹具&amp;quot;作为开工的门槛。放不进去就先解依赖——磨刀的时间不是浪费，是这笔任务里 ROI 最高的部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二次祈祷式修改就停下来补安全网。&lt;/strong&gt; 第一次 edit and pray 可以是形势所迫；同一个模块第二次还想凭感觉改，就是明确的信号——停下来，给这个模块写 characterization test，把网补上再走。恶习都是在&amp;quot;就这一次&amp;quot;里养成的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新逻辑一律 Sprout，从第一行起可测。&lt;/strong&gt; 往没测试的老代码里加功能时，默认走 Sprout Method / Sprout Class：老代码只加一行调用，新逻辑活在有测试的新方法新类里。让系统的&amp;quot;非遗留面积&amp;quot;随每次提交单调递增。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评审时多问一句：这次改动的防护网在哪？&lt;/strong&gt; Code review 除了看 diff 本身，固定问一句&amp;quot;这个行为现在有测试钉住吗&amp;quot;。没有，就讨论要不要当场补——这句话问上一年，团队的测试文化就长出来了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重构与改功能，永远不放在同一次提交。&lt;/strong&gt; 一次只动一种东西：要么纯重构（行为逐字节不变，测试不该红），要么纯改功能（在已绿的测试看护下改）。混在一起出问题时分不清归因，等于主动放弃了测试网的保护。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一句话总结这本书：&lt;strong&gt;测试不是成本，是让你敢动代码的许可证；seam 不是技巧，是代码的解剖学。&lt;/strong&gt; 读完它再看任何一个老系统，你看到的不再是&amp;quot;一坨烂代码&amp;quot;，而是一张还没铺开的防护网，和网上一个个等着被找到的接缝。&lt;/p&gt;
&lt;p&gt;最后再补一句这本书在我心里的真实位置：它不是&amp;quot;教你写测试&amp;quot;的书，而是&lt;strong&gt;教你在不信任代码的环境里重建信任&lt;/strong&gt;的书——对代码的信任、对改动的信任、最终是对自己手艺的信任。这份信任的重建方法，二十年来没有变过：让行为可见，让变化可测，让每一步可回退。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;相关阅读：&lt;a href=&#34;https://ftmao.com/blog/blog/bookstore/code-complete-learning-notes/&#34;&gt;《代码大全》学习笔记&lt;/a&gt; · &lt;a href=&#34;https://ftmao.com/blog/blog/bookstore/clean-architecture-book-notes/&#34;&gt;《整洁架构》读书笔记&lt;/a&gt; · &lt;a href=&#34;https://ftmao.com/blog/blog/posts/design-patterns-intro/&#34;&gt;设计模式入门&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这三篇和本文正好凑成一条线：设计模式教你&amp;quot;缝应该留在哪&amp;quot;，整洁架构教你&amp;quot;依赖应该指向哪&amp;quot;，代码大全教你&amp;quot;复杂度应该怎么管&amp;quot;，而本文回答的是那个最狼狈也最常见的问题——前面这些都没做到，系统已经在手里了，明天还要改，怎么办。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
