<?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%81%97%E7%95%99%E4%BB%A3%E7%A0%81/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Sat, 08 Jul 2023 10:40:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E9%81%97%E7%95%99%E4%BB%A3%E7%A0%81/index.xml" rel="self" type="application/rss+xml" /><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>
