用镀金玫瑰练习重构:先用测试守住行为,再消灭坏味道
课程收尾时,我没有再让同学们从零生成应用,而是给出经典的 Gilded Rose 遗留代码练习。代码能运行,却充满嵌套判断、重复分支和令人费解的命名。AI 很容易给出一句“我已经重构完成”,但真正困难的是:我们如何证明它没有改变原有行为?
本文采用社区常见译名“镀金玫瑰”。练习的重点不是得到一份公认最漂亮的代码,而是建立一条可审查、可回退、行为受保护的重构路径。
前置条件
- 从 Gilded Rose Refactoring Kata 仓库选择熟悉的语言版本。
- 确认项目原始测试命令能够运行。
- 不在第一步修改生产代码。
- 为练习建立独立 Git 分支,并保存初始基线。
第一步:先理解外部行为,不急着评价写法
先读需求说明、入口和已有测试,列出商品名称、销售剩余天数和品质变化之间的规则。此时可以让 AI 用 Ask 只读分析,但要求每条结论对应代码或测试证据。
遗留代码里的命名和结构可能很差,当前行为也未必完全符合你的直觉。重构的第一目标是保持已经存在且需要保留的外部行为;如果要修业务缺陷,应作为另一项需求单独处理。
第二步:建立可执行的行为基线
先运行现有测试并记录结果。若覆盖不足,为代表性规则补充特征测试:普通商品、Aged Brie、Backstage passes、Sulfuras,以及销售日期前后和品质上下界。
可以使用参数化表格表达输入和预期,但每一行都应独立可读。不要把几十个断言藏进依赖共享状态的循环,也不要把当前实现逐行复制到测试里。测试描述的是可观察行为,而不是替旧代码写注释。
如果某条规则无法从需求或现有行为判断,先记录未知并提问,不让 AI 自行补成“最佳实践”。
第三步:识别坏味道,选择一个最小切口
常见问题包括:
- 多层嵌套条件掩盖了业务规则。
- 同一种边界判断在多个分支重复。
- 技术性名称没有表达商品策略。
- 一个函数同时负责分类、计算和更新状态。
- 新增一种商品必须修改大量旧分支。
不要一次消灭所有问题。第一步可以只是提取一个有业务含义的小函数、消除一段重复条件或用提前返回降低嵌套。改动后立即运行测试并提交。
第四步:用小步循环约束 AI
给 Agent 的任务可以写成:
当前测试全部通过。只提取普通商品品质更新逻辑,不改变公共接口和任何输出;完成后运行测试,列出实际修改文件和仍存在的坏味道。
每一轮遵循:选择一个坏味道、做一个结构变化、运行测试、查看差异、提交 Git。这样即使某轮方向错误,也只需要回退一个小提交,而不是在大规模改写里寻找行为变化。
第五步:结构方案没有唯一答案
当不同商品拥有稳定且独立的更新策略时,策略对象、子类或工厂可能让新增规则更清楚;也可以用数据驱动表、映射函数或更小的纯函数完成。继承和工厂只是候选方案之一,不是这道题的唯一标准答案。
选择方案时问四个问题:
- 新增规则要修改多少旧代码?
- 每条业务规则能否在一个位置读懂?
- 测试失败时能否快速定位?
- 当前项目规模是否值得引入额外抽象?
过早建立复杂类层级,同样可能把简单规则藏进更多文件。重构目标是可理解和可变化,而不是使用最多设计模式。
第六步:用证据确认只改变结构
完成后至少检查:
- 所有原有与新增特征测试通过。
- 公共接口、输出和边界行为未改变。
- Git 差异中没有顺手加入新需求。
- 函数与变量名称更接近业务语言。
- 新增一种商品时,修改范围比重构前更可控。
测试绿色不是全部。还需要人工评审抽象是否真的降低理解成本,是否把重复转移到了别处,以及测试是否只是在迎合新实现。
成功标准
- 重构前已有可执行、可说明的行为基线。
- 每次只做一类结构改动,并立即运行相关测试。
- 任何时刻都能回退到最近可用提交。
- 重构没有混入功能变化或需求修复。
- 最终代码更接近业务语言,复杂度有清楚下降。
- 能解释所选结构的权衡,而不是只说“AI 推荐”。
常见报错与处理
AI 直接重写整个函数
停止并缩小任务。保留测试,要求只做一次提取、改名或去重;审查差异后再进入下一轮。
测试全部通过但不敢确认行为
检查覆盖是否只包含正常路径。补销售日期边界、品质上下限和各类特殊商品的特征测试,再审查是否存在未记录规则。
为每种商品建立很多空类
回到变化原因和项目规模。如果类层级没有减少条件判断或提高新增规则的局部性,纯函数或映射可能更直接。
重构时顺手修了业务问题
把行为修改拆到独立分支或提交,先确认需求和新验收,再实施修复。不要让结构变化和业务变化共享同一次评审。
课后练习
选择 Gilded Rose 中最难读的一段,建立行为测试后完成三次不改变行为的小重构,每次独立提交。最后比较第一次和最后一次代码,写下一个被消除的坏味道、一个仍然保留的问题,以及为什么没有继续抽象。