读书笔记 · 约 19,245 字 · 预计推送 2 部分

《修改代码的艺术》精选精读:接缝、依赖打破与特征测试 —— 结合个人历史项目的可执行反思 未开始

目录

题记与说明

主题研究 | 精读日期:2026-11-05 | 精读员:#2 | 前缀:knowledge-d2f37j93-2
原书:Michael C. Feathers, *Working Effectively with Legacy Code*, Prentice Hall PTR, 2004(ISBN 0-13-117705-2)
交付物:七维提取 + 接缝/依赖打破/特征测试三主题逐条原文引述 + 结合本机真实历史项目(QMT 量化脚本、工作回顾工具、douyin 前端)的可执行反思清单

0. 来源清单与检索方式(网络可用,全部在线精读)

#来源性质作用
1Pearson 官方样章 PDF(0131177052.pdf,56 页)一手:出版社官方样张,含前言 xvii(遗留代码定义)、全书 TOC、第 4 章 The Seam Model 全文(书页 29–44)著名定义、接缝定义/使能点侧栏、CAsyncSslRec 案例的逐字原文
2InformIT: Testing Effectively With Legacy Code — Seams(Feathers 本人,2005-01)一手:作者本人在出版社网站连载的第 4 章书摘接缝三类型的取舍原话;#ifdef 的警告
3understandlegacycode.com — The key points of Working Effectively with Legacy Code(Nicolas Carlo)二手:逐章导读博客全书工作流(sprout/wrap/insertion point)与特征测试的同名技术梳理
4understandlegacycode.com — Characterization Tests or Approval Tests?二手:术语辨析 + Emily Bache 四段式流程特征测试的操作流程、golden master 同源性、快照滥用的批评
5ACCU Overload 20/108 — Refactoring Towards Seams in C++(Michael Rüegg, 2012)二手(含原创扩展):C++ 实践视角"缺使能点即非接缝"反例;compile seam 扩展;link/preprocessing seam 的工程局限

检索执行记录(本会话实际运行,均成功):


一、核心问题

这本书回答的是四个环环相扣的问题:

  1. 遗留代码两难(Legacy Code Dilemma):要安全地改代码需要先有测试;要把测试装进现有代码又得先改代码。先有鸡还是先有蛋?(Carlo 的导读将其表述为 "you need tests before changing code, but need to change code to get tests in place"——来源 3)
  2. "一整张文本纸"困境:Feathers 观察到 "programming languages just don't seem to support testing very well",程序在开发者眼里是一张改一字动全身的大纸,缺少"哪里可以换行为而不动那里"的心智模型。(来源 1,书页 29)
  3. 行为未知时怎么写测试:面对"我不知道这段代码现在到底干什么、更不知道该写什么断言"的代码,如何建立安全网——这是第 13 章 Characterization Tests 的标题问题(TOC 证实:书页 185:"I Need to Make a Change, but I Don't Know What Tests to Write")。
  4. 没有时间怎么办:截止日期压顶、不可能做理想重构时,如何用最小侵入的方式让新代码可测——第 6 章 Sprout/Wrap(TOC 证实:书页 57–76)。

二、主要主张

  1. 遗留代码 = 没有测试的代码。这是定义性主张,作者明知有争议仍坚持:"To me, legacy code is simply code without tests. I've gotten some grief for this definition... It is a good working definition, and it points to a solution."(来源 1,前言 xvii)。配套侧栏断言:"Code without tests is bad code. It doesn't matter how well written it is; it doesn't matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don't know if our code is getting better or worse."(同页侧栏)
  2. 接缝(seam)模型:行为可替换点是一个可以独立于"读懂全部代码"来定位的对象。"A seam is a place where you can alter behavior in your program without editing in that place."(来源 1,书页 31 侧栏)
  3. 每个接缝必有使能点(enabling point):"Every seam has an enabling point, a place where you can make the decision to use one behavior or another."(来源 1,书页 36 侧栏)。反过来,没有使能点的调用就不是接缝——书中的 cell.Recalculate() 反例(书页 41)与 Rüegg 的 GameFourWins.play() 反例(来源 5)互相印证。
  4. 三类接缝有明确的优先级:"In general, object seams are the best choice in object-oriented languages.";preprocessing/link 接缝"are not as explicit as object seams",作者"I like to reserve preprocessing seams and link seams for cases where dependencies are pervasive and there are no better alternatives."(来源 2)
  5. 特征测试记录"是什么"而非"应该是什么":"The purpose of characterization testing is to document your system's actual behavior; not to check for the behavior you wish your system had"(Feathers 原话,经来源 4 引述);"A characterization test is a test that characterizes the actual behavior of a piece of code."(来源 3)
  6. 依赖打破技术是一类特殊重构:前言明确其性质——"The refactorings in Dependency-Breaking Techniques are special in that they are meant to be done without tests, in the service of putting tests in place."(来源 1,书页 xxi)——即在无测试状态下为了上测试而做的、必须格外小心的一次性手术。
  7. 接缝视角改变"好设计"的定义:"The seam view of software helps us see the opportunities that are already in the code base."(来源 1,书页 33);把类从项目里抽出来测时,"often you have a lot of work to do, regardless of how 'good' the design is"(同页)。

三、方法(这本书如何论证)

四、数据与证据(关键原文引述,全部本会话逐字核对)

4.1 遗留代码定义与代价(来源 1,前言 xvii)

"In the industry, legacy code is often used as a slang term for difficult-to-change code that we don't understand. But over years of working with teams, helping them get past serious code problems, I've arrived at a different definition. To me, legacy code is simply code without tests."
"Teams with that degree of clarity in all of their code are rare. They seem like a statistical anomaly. And, you know what? If they don't have supporting tests, their code changes still appear to be slower than those of teams that do."
(侧栏)"Code without tests is bad code. It doesn't matter how well written it is; it doesn't matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don't know if our code is getting better or worse."

4.2 接缝与使能点定义(来源 1,PDF 行 903 / 1128–1131;来源 2 同文)

(侧栏 Seam,书页 31)"A seam is a place where you can alter behavior in your program without editing in that place."
(侧栏 Enabling Point,书页 36)"Every seam has an enabling point, a place where you can make the decision to use one behavior or another."
(书页 36)"When you have a seam, you have a place where behavior can change. We can't really go to that place and change the code just to test it. The source code should be the same in both production and test."

4.3 三类接缝要点(来源 1 书页 33–42;来源 2)

4.4 CAsyncSslRec 案例(来源 1,书页 30–33)

同一处全局调用 PostReceiveError(SOCKETCALLBACK, SSL_FAILURE)(与另一子系统通信、测试中很痛),三种解法原文:

  1. 对象接缝:把全局函数包成同名 virtual 方法 + 空实现子类 TestingAsyncSslRec——"We were able to change the method that is called without changing the method that calls it"(书页 33)。
  2. 链接接缝:测试库里给同名全局函数空体;"The enabling point would be our makefile or some..."(书页 44 区域,PDF 行 1429)。
  3. 预处理接缝:测试头文件里 #define 同名宏。

4.5 传感(sensing)模式(来源 1,书页 39–40)

用队列记录调用以"感知"副作用,最小可复制的模式:

std::queue<GraphicsAction> actions;
void drawLine(int firstX, int firstY, int secondX, int secondY) {
    actions.push_back(GraphicsAction(LINE_DRAW, firstX, firstY, secondX, secondY));
}
// 测试:LONGS_EQUAL(5, actions.size()); 并逐个断言 action.type / 坐标

作者提醒:"it is best to start with a very simple scheme and allow it to get only as complicated as it needs to be."(书页 40)

4.6 特征测试与依赖打破(来源 3、4,二手)

"A characterization test is a test that characterizes the actual behavior of a piece of code."(来源 3)
"The purpose of characterization testing is to document your system's actual behavior; not to check for the behavior you wish your system had"(Feathers 原话经来源 4 引述)

来源 4 的三阶段流程与验证技巧:

Sprout / Wrap(来源 3):

4.7 C++ 工程视角的补强与反驳(来源 5)

证据质量评估:4.1–4.5 为一手书文(出版社官方 PDF + 作者本人书摘,页码可查);4.6 为高质量二手(逐章导读,含作者引语转引);4.7 为独立第三方实践(有代码与工具链约束,可复现)。三组证据在"缺使能点即非接缝""link seam 使能点在代码外"两点上独立收敛,交叉验证充分。

五、结论

  1. 接缝模型的本质:把"这段代码能不能安全改"从对整个代码库的设计评价,降维成对单个调用点的局部判断——看它有没有使能点。没有就先造一个(通常一次 Extract & Override 或参数化构造),造接缝的过程本身就是依赖打破。
  2. 三个主题是一条流水线:接缝(找到能换行为的地方)→ 依赖打破(最小侵入地把地方造出来,Sprout/Wrap/Extract…)→ 特征测试(在换上测试替身之前,先用真实行为填满断言)。顺序不可颠倒:特征测试是让接缝上的替换"可验证"的前提。
  3. 特征测试的正确姿势:它记录现状、包括 bug;发现 bug 的正确动作是先写一个证明 bug 存在的特征测试锁定它,再修,而不是边测边修(来源 4 的工作流隐含此纪律;快照滥用的反面教训是"people just update snapshots without really understanding what's going on")。
  4. 语言决定接缝菜单:OO 语言优先对象接缝;C++ 额外有模板(compile seam);C/脚本语言常只能用链接/预处理接缝或"参数列表即接缝"。使能点在代码外(构建脚本、classpath、环境变量)的接缝威力大但难被注意到——代码评审时容易漏。
  5. 对个人项目的直接推论:见第七节——一个工具不需要"有完整测试"才算健康,只需要要改的那条路径上有接缝和特征测试;反向地,把 subprocess/时钟/网络收敛到单一封装层,等于提前给未来的自己造好接缝。

六、局限

  1. 2004 年的技术底座:Java 例子依赖 classpath 手工替换,今天 Mock frameworks(Mockito/Byte Buddy)与 JDK 工具已把 link seam 的大部分场景收编成 API 调用;Python/JS 这类动态语言里 monkeypatch/模块 mock 让"对象接缝"几乎免费——书中"打破依赖很难"的部分论证力度随语言演化衰减。C++ 部分未覆盖 C++11 以后的 lambda/std::function 注入(Rüegg 提到 C++11 但未展开)。
  2. 定义的争议性:作者自己承认被 grief("I've gotten some grief for this definition")。"没测试 = 坏代码"对一次性脚本、探索性 notebook、被平台加密无法携带测试的代码(本机 QMT 即是,见 7.1)不成立——更准确说是"没有测试的正在演化的代码"。
  3. 书中没有给"特征测试遇到非确定性"的完整方案:时间、随机、网络抖动会让 golden master 不稳定;来源 4 只给了"Printer 滤掉易变数据"的方向性建议,没有可操作的通用解(现代方案:快照可调字段 / 假时钟,书里未涉及)。
  4. 架构级遗留系统覆盖不足:接缝是调用点级的显微镜,缺少跨服务、数据迁移、schema 兼容这类"行为特征化"的技法(第 12 章 pinch point 是最接近的,TOC 证实书页 182,但样章未含正文)。
  5. 依赖打破技术的"临时性"风险:Sprout/Wrap 刻意绕开测试动手术,作者在书页 xxi 说它们是 "in the service of putting tests in place"——但实践里极易变成永久性的旁路结构;书本身没有强力的反退化机制(依赖命名约定与评审纪律)。
  6. 本笔记的边界:样章只含前言+Ch1+Ch4,第 13 章特征测试正文(书页 185–196)与 Part III 目录(书页 ~427 起)未获一手文本,特征测试流程引自二手导读;未运行书中任何代码示例;QMT/工作回顾项目的"反思"是静态分析,未实际写测试验证可行性(7.5 清单留作执行项)。

七、对我的启发 —— 结合个人历史项目的可执行反思

7.0 项目取证(本会话实跑命令与事实)

项目位置本会话取证事实
国金 QMT 量化策略D:/国金QMT交易端模拟/python/find ... -name "*.py" | wc -l → 33;find ... -iname "*test*" -name "*.py" → 0;二进制头分类脚本33 个 .py、0 个测试;14 个明文(GBK,如 ARIMA预测.py 69 行,ContextInfo×13、handlebar×2、get_market_data×1);19 个被平台加密(头部 R3/RS 或整文件 base64 块,含用户改进量最大的 期货网格.py 71KB、增强网格策略V2.py 58KB、期权网格.py 30KB)——源码在本机不可读
个人工作回顾工具C:/Users/马鑫悦/Documents/工作回顾/读 scripts/generate_work_review.py 统计930 行 / 41 个函数 / 0 测试引用 / datetime.now 仅 1 处;函数名清单显示分层清晰:parse_args/parse_date/build_window(纯逻辑)→ load_config/parse_simple_yaml → run_git/discover_git_repos/collect_commits(副作用)
douyin 数据看板前端D:/projects/douyin_dashboard_frontend/读 package.json + find -name "*.test.*" -not -path "*/node_modules/*" | wc -l24 个测试文件、vitest run、precommit:check = lint + tsc --noEmit + test——已经是"非遗留代码"的对照组
个人博客D:/my-blog/(Hexo)ls;项目盘点 _索引.md 记载"2025-11 后未动"低频变更,不需要接缝投资

7.1 QMT 量化脚本:加密源码倒逼"记录级特征测试"

这是全书理念的一个尖锐反例:源码被平台加密,19 个策略文件(恰是用户投入最多、最想改的网格策略族)在本机连 import 都做不到——Feathers 意义上的所有源码级接缝(对象/链接/预处理)全部失效。但这不等于只能裸奔:

  # 在 D:/国金QMT交易端模拟/export_data/ 建立母带目录(该目录已存在,本会话 ls 验证)
  # 每次"改参数前的最后一次成功回测"导出后:
  cp export_data/回测导出.csv "export_data/golden/$(Get-Date -Format yyyyMMdd-HHmm)_策略名_参数指纹.csv"
  # 1. 把 D:/国金QMT交易端模拟/python/ARIMA预测.py 复制为 tests/fixtures/ 下的样本
  # 2. 新建 D:/CodexData/qmt-strategy-tests/(git 管理),pip install pytest
  # 3. 第一支特征测试:调 handlebar(fake_context),把输出的序列 print 出来锁定成 golden 文件

7.2 工作回顾工具:教科书式的"先接缝后特征"练习场

generate_work_review.py 的函数分层恰好是 Feathers 想要的改造地形:

  1. run_git 是天然接缝(subprocess 封装,全部 git 交互的咽喉):按书页 42 的模式,把 run_git 做成可注入参数——collect_commits(repo, run=run_git),默认真实现,测试传 fake。参数列表即使能点,一行签名改动完成对象接缝。
  2. parse_simple_yaml / build_window / parse_date 是零风险起步点:纯函数、无副作用,特征测试只需喂输入锁输出。930 行的工具,这三支测试就能覆盖配置解析最常见的回归。
  3. datetime.now 全文件仅 1 处:时钟依赖已经收敛——这是本会话取证里最幸运的发现。把这一处改成可注入(或 monkeypatch),窗口逻辑就能确定性测试。
  4. 纪律对照:这个工具"活跃使用中(logs 持续更新到 2026-10-24,见 D:/projects/_索引.md)",即每周末报告都依赖它。按书的定义它是遗留代码(无测试),且正在被每次"改报告格式"反复触碰——正是"改动频率 × 无测试"风险矩阵里该优先动手的那个。

7.3 douyin 前端:对照组的启发是"守住接线"

24 个测试文件 + precommit:check 三连(lint + tsc + test)说明这个项目已经在书定义的"非遗留"一侧。两个仍然值得做的反思:

7.4 给新项目的三条"预埋接缝"规则

  1. 副作用收敛到单一封装层:subprocess、网络、时钟、文件系统各只允许一个出口函数——未来的接缝天然存在,不用再"打破依赖"。
  2. 构造函数参数即接缝:写新类时把协作者从构造函数传入(哪怕默认实现就一行),这是书页 42 模式的零成本预埋。
  3. 变更先问"在哪断言":动任何超过 20 行的函数前,先回答"改坏了什么测试会红"。答不出来 → 先花 10 分钟在该函数出口处加特征测试(print 锁定 → assert),再动刀。

7.5 可执行清单(下一步照做)

八、术语速查表

术语定义(原文)出处
Seam"a place where you can alter behavior in your program without editing in that place"书页 31
Enabling point"a place where you can make the decision to use one behavior or another"书页 36
Preprocessing seam使能点 = 预处理宏(如 TESTING);保留给"dependencies are pervasive"的场合书页 33–36;InformIT
Link seam使能点"always outside the program text"(构建脚本/classpath)书页 37–40
Object seamOO 中"call 不决定执行哪个方法";使能点常是参数列表/构造点书页 40–44
Compile seam(Rüegg 扩展)模板参数静态注入,使能点 = 实例化处ACCU
Sprout Method/Class新逻辑写到独立可测处,回插插入点书页 59/63(TOC)
Wrap Method/Class改名旧方法→原名新方法包住它→新逻辑放调用前后书页 67/71(TOC)
Interception point一片代码的多处公共汇合点,判断该在哪写测试书页 174(TOC)
Pinch pointinterception point 中"输入变化最能暴露输出错误"的窄口书页 182(TOC)
Characterization test"a test that characterizes the actual behavior of a piece of code"书页 186(TOC)+ 来源 3
Golden Master / Approval同一技术的音频母带隐喻:"the one truth from which copies can be made"来源 4

附:来源链接汇总

*本笔记所有 PDF 引语由 pypdf 从官方样章逐字提取(sample_text.txt,56 页 101,135 字符),引语旁标注书页码;二手引语经 WebFetch/webReader 全文抓取。项目取证命令与输出见 7.0 表,全部为本会话实跑。*

← 资料库今日播报 →