Chapter 6:Incremental Design
对齐原书 Chapter 6:用简单设计力量指导每轮重构,区分低可逆风险的前置设计与高可逆结构的增量演化,并识别慢测试、共享状态、缺少安全网和大批变更等重构阻碍。
学习目标
- 能按通过测试、表达意图、消除重复和最少元素四项力量审查当前设计,而不是把“简单”误解成最短代码
- 能按风险和可逆性区分必须提前验证的协议/选型与可由红绿重构逐步演化的内部结构
- 能识别并治理重构阻碍,通过快测试、独立状态、特征测试和小批变更恢复持续设计能力
机制总览
Chapter 6:Incremental Design:机制路径
- 1
设计不是编码前的一次活动
TDD 不取消设计,它改变设计发生的节奏。开始前仍要理解业务目标、外部协议、质量属性和不可逆风险;编码中则用每个新例子和重构持续校正类、函数与依赖边界。最终结构来自多次有证据的选择,而不是第一张类图冻结的猜测。
- 2
简单设计有优先顺序
“简单”不是行数最少。把税率公式复制进三个 if 可能很短,却让知识散落;引入七层抽象也消除重复,却让当前需求难以理解。先保证行为,再让名字和边界直接,随后统一重复知识,最后删除没有当前价值的元素。
- 3
先预测:哪项决策值得现在冻结
在继续阅读前,把“公开消息 schema、内部 helper 名称、p95 延迟预算、数据库产品、日志字段顺序”分别标成低、中或高可逆,并写下验证方法。读完本章后重新分类:若理由只有“以后也许需要”,它不是前置设计证据;若错误选择会影响外部消费者、安全或迁移成本,就应先做最薄试验。
章级决策实验
Chapter 6:Incremental Design:机制与证据
切换《Chapter 6:Incremental Design》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 设计不是编码前的一次活动
TDD 不取消设计,它改变设计发生的节奏。开始前仍要理解业务目标、外部协议、质量属性和不可逆风险;编码中则用每个新例子和重构持续校正类、函数与依赖边界。最终结构来自多次有证据的选择,而不是第一张类图冻结的猜测。
可核验证据
保存红—绿—重构的最小提交与失败消息,用行为断言、替身交互和重复运行核对「设计不是编码前的一次活动」是否提供快速反馈。
学完《Chapter 6:Incremental Design》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 6:Incremental Design:失效与核验
设计不是编码前的一次活动
典型失效
若把「设计不是编码前的一次活动」退化成先写实现再补断言,测试会耦合内部步骤,既不能驱动设计,也无法稳定解释失败。
核验证据
保存红—绿—重构的最小提交与失败消息,用行为断言、替身交互和重复运行核对「设计不是编码前的一次活动」是否提供快速反馈。
简单设计有优先顺序
典型失效
若把「简单设计有优先顺序」退化成先写实现再补断言,测试会耦合内部步骤,既不能驱动设计,也无法稳定解释失败。
核验证据
保存红—绿—重构的最小提交与失败消息,用行为断言、替身交互和重复运行核对「简单设计有优先顺序」是否提供快速反馈。
先预测:哪项决策值得现在冻结
典型失效
若把「先预测:哪项决策值得现在冻结」退化成先写实现再补断言,测试会耦合内部步骤,既不能驱动设计,也无法稳定解释失败。
核验证据
保存红—绿—重构的最小提交与失败消息,用行为断言、替身交互和重复运行核对「先预测:哪项决策值得现在冻结」是否提供快速反馈。
设计不是编码前的一次活动
TDD 不取消设计,它改变设计发生的节奏。开始前仍要理解业务目标、外部协议、质量属性和不可逆风险;编码中则用每个新例子和重构持续校正类、函数与依赖边界。最终结构来自多次有证据的选择,而不是第一张类图冻结的猜测。
↡在短反馈保护下,随着行为与知识增加逐步调整职责、接口和依赖结构的持续设计方式。增量不等于随机。每一轮都要有方向:当前测试证明行为,代码表达意图,同一知识只有一个来源,没有无需求证据的结构。方向稳定,具体抽象允许演化。
简单设计有优先顺序
↡以通过全部测试、清楚表达意图、消除知识重复和保持最少必要元素为约束的当前最优结构。“简单”不是行数最少。把税率公式复制进三个 if 可能很短,却让知识散落;引入七层抽象也消除重复,却让当前需求难以理解。先保证行为,再让名字和边界直接,随后统一重复知识,最后删除没有当前价值的元素。
先预测:哪项决策值得现在冻结
在继续阅读前,把“公开消息 schema、内部 helper 名称、p95 延迟预算、数据库产品、日志字段顺序”分别标成低、中或高可逆,并写下验证方法。读完本章后重新分类:若理由只有“以后也许需要”,它不是前置设计证据;若错误选择会影响外部消费者、安全或迁移成本,就应先做最薄试验。
四项力量有冲突时按风险判断。为消除两行表面重复而创建含糊 ManagerFactory,会损害意图;相同税务规则散落多处则是知识重复,应尽早集中。重构后仍需全绿,证明结构变化没有改写行为。
Money Invoice::total() const {
return linesTotal() + taxPolicy_.taxFor(linesTotal());
}这里 Invoice 表达汇总,TaxPolicy 表达可变化规则;若系统目前只有一个固定税率且没有第二个变化证据,直接函数可能更简单。抽象是否成立看变化原因和调用意图,不看设计模式名称。
测试通过只是设计下限
测试不可能证明所有质量。两份重复实现可以通过同一测试,晦涩名字也能算出正确结果。绿灯提供安全网,重构阶段仍需人工判断意图、重复、耦合、生命周期和错误语义。
↡测试反馈与代码可读性共同提供的信息,用来判断责任是否需要移动、抽象是否成立或结构是否应删除。代码评审应同时问:测试为何存在,生产名字是否直接表达规则,依赖方向是否符合领域,新增抽象是否由至少一个真实变化压力支持。TDD 自动化反馈不能替代这些问题,但让回答后的改动更安全。
前置设计解决低可逆和跨边界风险
↡在实现细节展开前,对外部契约、质量属性、技术风险和不可逆决策进行的必要探索与约束。数据库迁移格式、公开协议、隐私边界、实时预算和第三方平台限制不能等到最后才发现。先用短 spike、兼容性样例、基准或 walking skeleton 验证风险,再把结论变成自动化契约。前置设计输出应是已验证约束和待决问题,不是试图预测所有类。
约束:旧客户端仍发送 schema v1
证据:v1/v2 契约样例均通过 parser contract
预算:p95 编码延迟低于 5 ms
证据:代表数据基准进入 CI 趋势记录
待决:内部缓存结构,由行为和基准增量选择越难回滚、影响范围越大、学习成本越高的决策,越值得提前验证;类内 helper、局部算法和命名通常高可逆,可留给短循环。
spike 的代码不直接成为生产实现
spike 以获取知识为目标,可以临时忽略结构、错误处理或测试。完成后保存测量、API 限制和决策,丢弃探索代码,再从测试驱动生产路径。若直接“清理一下”就上线,未受约束的捷径会成为长期设计。
walking skeleton 则不同:它是极薄但可部署、可测试的端到端路径,保留在产品中。它验证真实边界,随后内部行为继续增量扩展。
重构把新知识反馈到结构
↡在外部可观察行为不变且测试持续通过的前提下,改善代码内部结构的受控变换。常见重构包括改名、提取函数/类、移动方法、替换条件为多态、收窄接口、引入参数对象。每一步应足够小,失败时能立即定位。不要等“功能全部完成”再重构,因为重复和错误责任会持续放大下一步成本。
// 行为测试已证明不同地区税率后,再提取策略。
class TaxPolicy {
public:
virtual ~TaxPolicy() = default;
virtual Money taxFor(Money subtotal) const = 0;
};如果只有一个地区和固定规则,提取接口可能过早;当第二个地区测试使条件扩散,策略边界才有真实压力。不是机械要求“第三次重复才抽象”,而是观察重复的知识和变化方向。
重构阻碍会让设计停止演化
↡使开发者无法快速、确定地验证结构变化,从而害怕或推迟重构的技术与流程条件。重构阻碍包括慢测试、随机失败、共享全局状态、缺少行为安全网、大批长生命周期分支和无法一键构建;它们都会扩大反馈距离。团队表面上“没有时间重构”,实质是每次结构变化的验证成本过高。
治理顺序先恢复可诊断信号:让旧测试稳定,隔离进程外资源,缩短快层,给无测试区域加特征测试,再移动结构。直接在红灯和随机失败上大改,会让任何新失败失去归因。
特征测试为遗留行为建立临时护栏
当代码无测试且行为不完全明确,先写 characterization test 记录当前可观察结果,即使结果看起来奇怪。它不是批准 bug,而是把现状变成可见契约;若要修复 bug,另写描述期望的新失败测试。
TEST(LegacyPriceTest, CharacterizesCurrentRoundingAtHalfCent) {
LegacyPrice price;
EXPECT_EQ(101, price.totalCents(1005, 10));
}有了护栏后再提取纯计算、显式依赖或拆分长函数。特征测试应逐步由更清晰的领域测试替代,不要永久锁死偶然实现细节。
重复分为文本重复与知识重复
两段相似代码可能代表不同领域规则,过早合并会让未来变化互相牵制;两段文本不同却编码同一折扣阈值,则是危险知识重复。重构要问“它们为何相同、是否会因同一原因变化”,而不是只看字符。
测试代码也适用。把每个测试压缩进复杂 DSL 可能消除文本重复,却隐藏输入与期望;保留少量 setup 重复有时更能表达行为。简单设计的意图优先于机械 DRY。
过度前置设计的信号
↡在没有当前行为、风险或变化证据时提前确定大量抽象、扩展点和实现结构,导致反馈被猜测取代。典型信号包括:只有一个实现却有多层 factory;接口包含“以后也许用到”的方法;配置开关没有测试场景;领域名字被 Manager、Processor、Helper 淹没;新增简单行为要穿过多个无逻辑层。
删除无证据元素也是设计。先由测试证明外部行为,再内联一层、移除未用扩展点或合并空壳类型,每步全绿。代码库越小,未来添加真正变化边界的成本通常越低。
性能与增量设计并不冲突
先定义可测性能预算和代表负载,不要凭直觉提前微优化。保持简单实现,通过 profiler 找到热点,再在基准保护下改变算法或布局。性能目标可前置,优化结构应由测量增量驱动。
static void EncodeSoundex(benchmark::State& state) {
Soundex soundex;
for (auto _ : state) {
benchmark::DoNotOptimize(soundex.encode("Washington"));
}
}
BENCHMARK(EncodeSoundex);基准不是单元断言,机器噪声和趋势需要专门处理。它提供性能设计反馈,而功能测试继续证明结果语义。
设计日志保存决策原因
对低可逆决策记录上下文、选择、证据、替代方案和复查触发条件。日志不需要长文;目标是未来知道为何固定协议、为何选某存储,以及什么变化会使决策失效。高可逆局部重构由代码、测试和提交历史说明即可。
设计日志和测试清单互补:日志保存跨期决策,清单选择下一行为。两者都应允许更新,不能成为冻结结构的权威。
第一步:按简单设计力量审查当前绿灯
先保持全部行为,再改善领域名字与责任,识别知识重复,最后删除没有当前价值的元素;不要用行数或模式数量衡量简单。
小结
- 增量设计让行为证据和新知识持续改变结构,不等于没有方向或拒绝架构
- 简单设计先通过测试,再表达意图、消除知识重复并保持最少必要元素
- 低可逆外部边界应前置验证,高可逆内部结构适合红绿重构演化
- spike 获取知识后丢弃探索代码,walking skeleton 是保留的最薄真实路径
- 慢测试、共享状态、无安全网和大批变更是重构阻碍,应先恢复可诊断反馈
- 特征测试保护遗留现状,性能预算与基准让优化也能增量设计
练习
- 问题 1:判断设计时机。 新系统同时要决定公开消息 schema 和内部折扣 helper。哪些应提前验证?
- 问题 2:识别简单设计冲突。 两段相似代码来自不同法规,合并后名字变成
GenericRuleProcessor。是否应该 DRY?
- 问题 3:恢复重构能力。 遗留模块无测试、全套又需十五分钟,团队不敢拆长函数。给出闭环顺序。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 增量设计
- 在短反馈保护下随行为和知识逐步调整结构的持续设计方式。
- 简单设计
- 通过测试、表达意图、消除知识重复并保持最少必要元素的结构。
- 设计反馈
- 测试结果与代码可读性共同提供的责任和抽象调整信息。
- 前置设计
- 实现前对外部契约、质量属性和低可逆风险进行的必要验证。
- 重构
- 在可观察行为不变和测试持续全绿下改善内部结构。
- 重构阻碍
- 使结构变化无法快速确定验证、从而被害怕或推迟的条件。
- 过度前置设计
- 在无当前证据时提前冻结大量抽象和扩展点的做法。