安全重构

本页提供重构前安全检查清单、四种原子重构操作的安全评级与提交粒度、行为保持验证的代码示例(snapshot vs golden file),以及 5 个不安全重构的 before/after 反例。

SKILL 禁止「一次 diff 重写模块」:要求列出重构前的不变量(公开 API、持久化格式、消息契约),并在每步后运行相关测试与静态检查。

对缺乏测试的遗留代码,先补最窄的特征测试或合约测试,再动结构;Agent 应标出哪些行为是推断的、需人工确认。

原则与边界

工具链建议:IDE 自动重构优先于手写替换;跨文件改名用语言服务器或 codemod,减少漏改。合并策略上偏好多个小 PR 而非巨型分支。

  • 重构 PR 不夹带行为变更与功能需求,除非显式拆分。
  • 性能敏感路径重构需前后基准或 profiling 对比。
  • 数据库与 API 变更走扩张—收缩,不单靠「一起发版」。

重构前安全检查清单

重构前必须检查(每项不通过则不允许开始重构):

覆盖率门禁:
  [ ] 当前模块单测覆盖率 ≥ 70%(运行:npm test -- --coverage --collectCoverageFrom='src/module/**')
  [ ] 若覆盖率不足,先补特征测试(黑盒 I/O 测试),再重构

回滚能力:
  [ ] 所有改动在独立分支上(git branch refactor/xxx)
  [ ] 每步原子提交,可 git revert 到任意中间状态
  [ ] 若涉及数据库迁移,确认 migration 可 down(执行:make db-rollback 验证)

依赖范围:
  [ ] 确认有多少文件依赖此模块(运行:grep -r "from.*module-name" src/ | wc -l)
  [ ] 对外公开 API 的变更列表已经确认(function signatures、error codes)
  [ ] 消费方服务是否需要同步升级(查 CODEOWNERS 通知相关团队)

四种原子重构操作及安全评级:

操作          | 安全级别  | 每步提交粒度
-------------|----------|--------------------------------------------
rename       | ★★★★★   | 一次只改一个符号,单独 commit
              |          | git commit -m "refactor: rename getUserData to fetchUserProfile"
extract      | ★★★★☆   | 先提取函数(保留原调用),测试绿后删原代码
              |          | git commit -m "refactor: extract validateCoupon from checkout handler"
move         | ★★★☆☆   | 先加 re-export 保持路径稳定,下一步再删 re-export
              |          | commit 1: "refactor: add re-export for backward compat"
              |          | commit 2: "refactor: update all imports to new path"
              |          | commit 3: "refactor: remove re-export after import migration"
inline       | ★★☆☆☆   | 内联前确认函数无副作用、无被多处调用
              |          | git commit -m "refactor: inline single-use helper formatDate"

行为保持验证:snapshot 测试 vs golden file 对比

// 方案 1:Jest/Vitest Snapshot 测试(适合 UI 组件和序列化输出)
it('checkout summary renders correctly', () => {
  const { container } = render(<OrderSummary order={mockOrder} />);
  expect(container).toMatchSnapshot();  // 重构前运行生成快照
});
// 重构完成后,若快照变了,运行 jest --updateSnapshot 并仔细 review diff

// 方案 2:Golden File 对比(适合 API 响应、数据转换、报表生成)
// golden-files/order-summary.json (重构前生成并提交到 git)
{
  "orderId": "ORD-001",
  "total": 99.50,
  "items": [{"name": "商品A", "qty": 2, "price": 49.75}]
}

// 测试代码
import expectedOutput from './golden-files/order-summary.json';
it('transformOrder output matches golden file', () => {
  const result = transformOrder(rawApiResponse);
  expect(result).toEqual(expectedOutput);  // 重构前后输出必须完全一致
});

// 什么时候用哪个:
// snapshot → 组件树、HTML 输出、对象序列化(快速但 diff 不易阅读)
// golden file → 数据转换、报表、API 响应(diff 更清晰,适合代码评审)

5 个不安全重构反例(Before / After)

❌ 反例 1:重命名时漏改反射/字符串引用
Before: function getUserData(id) { ... }
After:  function fetchUserProfile(id) { ... }
问题:routes.ts 里有 app.get('/user', handlers['getUserData'])  ← 字符串引用漏改
修复:用 IDE 全局重构,而不是手动 find & replace

❌ 反例 2:提取函数时改变了默认参数行为
Before: function formatDate(date, format = 'YYYY-MM-DD') { ... }
After:  function formatDate(date, format = 'MM/DD/YYYY') { ... }  ← 默认值被悄悄改了
修复:提取函数时不改任何默认值,用特征测试锁定默认行为

❌ 反例 3:搬移文件时更新了部分 import 但未全部更新
Before: import { helper } from '../utils/helper'
After:  import { helper } from '../shared/helper'  ← 只改了部分文件
问题:老 ../utils/helper 被删了,其他引用的文件编译失败
修复:先加 re-export(utils/helper.ts → export * from '../shared/helper'),
      再批量更新 import,最后删 re-export

❌ 反例 4:inline 函数时引入了重复副作用
Before: 
  function logAndReturn(val) { metrics.count++; return val; }
  const a = logAndReturn(x);
  const b = logAndReturn(y);
After(inline):
  const a = (metrics.count++, x);  // ← metrics.count 计数改变了
  const b = (metrics.count++, y);
修复:有副作用的函数不要 inline

❌ 反例 5:重构顺序倒置(先删旧代码再补测试)
Before: 直接删除老实现,新实现没有测试
问题:发现新实现有 bug 时,没有测试作为 safety net,回滚成本极高
修复:顺序必须是 补测试 → 实现新逻辑 → 验证测试绿 → 删旧代码

不变量与契约

不变量是「改完结构之后仍然必须为真」的陈述。动笔前先写成清单,便于审查与回滚时对照;对 Agent 应要求逐条引用代码或测试位置,而不是泛泛承诺「行为不变」。

  • 公开 API:函数签名、错误码、幂等语义;若需破坏兼容,单独 PR 与发布说明。
  • 持久化:表字段含义、序列化格式、迁移可逆性;版本号或 feature flag 的读写双方。
  • 消息与集成:队列主题、事件 schema、gRPC/REST 契约;消费者是否可灰度。
  • 可观测性:关键日志字段、指标名、告警查询;重构后查询语句仍应有效。

对无法验证的不变量(例如「依赖未文档化的第三方行为」),在 PR 描述里标为假设,并附验证计划或人工签收。

先测再搬

「搬移」指移动文件、改模块边界、调整依赖方向等易漏改的操作。顺序应是:行为已有测试锚点 → 再动目录与 import → 最后清理别名与兼容层

  • 理想情况:单元或集成测试已覆盖当前行为;若没有,先加最窄的特征测试(黑盒输入输出)或合约测试。
  • 搬移时优先保持符号路径稳定:必要时先加 re-export 或适配层,下一步再删,避免一步改十个引用点。
  • 每完成一次「只搬不移除逻辑」的提交,应绿构建;不要攒到「全部搬完再测」。

扩张—收缩(API 与数据)

对对外契约或共享存储的变更,用两(多)阶段发布降低耦合:扩张阶段新旧并存、向后兼容;收缩阶段在确认无调用方后删除旧路径。

  1. 扩张:新增字段/端点/表列,默认值与老客户端可读;老代码路径仍工作。
  2. 迁移:双写或回填脚本;监控错误率与数据一致性。
  3. 切换:读路径切到新实现(可配开关),观察一段时间。
  4. 收缩:下线旧 API、删列、移除死代码;此时才允许「破坏性」提交。

Agent 应显式写出当前处于哪一阶段;禁止在单 commit 里同时「加新契约又删旧契约」除非团队约定可原子发布。

小步提交流水线

每一步产出应是可审查、可 revert 的单元;理想情况下每一步都对应一条清晰的提交说明(见下文「提交信息一行」工具)。

  [ 列出不变量 + 风险面 ]
                    │
                    ▼
            [ 补测试 / 特征测试 锁定行为 ]
                    │
                    ▼
            [ 小步改动:IDE 重构 / 单意图编辑 ]
                    │
                    ▼
         ┌──────────┴──────────┐
         ▼                     ▼
  [ 本地:相关测试 + lint ]   [ 可选:类型检查 / 格式化 ]
         │                     │
         └──────────┬──────────┘
                    ▼
            [ 提交:单意图、可 revert ]
                    │
                    ▼
            [ 推送 → CI 全绿再下一步 ]

CI 门禁步骤

重构分支上的 CI 应与功能开发一致或更严:任何一步失败都阻止合并,避免「只在本地绿」。可按仓库体量裁剪,但顺序逻辑建议如下。

  1. 检出与依赖:锁定文件校验、安装可复现;避免「我机器能上」。
  2. 静态分析:lint、format 检查(或 format 机器人提交)。
  3. 类型检查:若有 TypeScript / mypy / 等,与构建同优先级。
  4. 单元与集成测试:与改动模块相关的套件;大仓可用 affected 策略。
  5. 构建产物:应用或库能完整构建;多包仓库验证依赖图未被破坏。
  6. (可选)契约 / E2E:动到 API 或关键路径时拉齐;数据库变更跑迁移干跑。

Agent 在描述「本步完成」时应引用具体命令或 CI job 名称,而不是仅称「测试过了」。

反模式

巨型 diff:单次 PR 混合改名、格式化、业务逻辑与依赖升级——审查无法聚焦,回滚成本高。应拆成机械提交(如「仅 format」)与语义提交。
无测试搬家:大段剪切粘贴后靠编译通过安慰自己;跨语言边界或反射调用处最易漏。先锚测试再动文件。
隐式行为变更:「顺便优化」改变排序、超时、默认参数或错误信息——对用户与下游都是行为破坏,须单独说明与测试。
与发布强绑定的契约删除:跳过扩张阶段直接删字段/端点,易导致灰度窗口外故障。按阶段发布并监控。

提交信息一行(前缀 + 摘要)

用前缀区分意图,便于日志筛选与 revert:refactor: 纯结构、test: 仅测试、chore: 工具与配置。下面选择前缀并填写英文或中文摘要,生成一整行;仅保存在本页浏览器(localStorage),键名与其他页面不共用。

前缀

            
---
name: refactoring-safety
description: 安全检查清单、原子重构操作、行为保持验证与不安全反例
---

# 步骤 1:重构前安全检查
执行: npm test -- --coverage --collectCoverageFrom='src/<module>/**'
门禁: 覆盖率 ≥ 70%,否则先补特征测试
确认: git branch refactor/<desc> 已创建,可随时 revert

# 步骤 2:列出不变量
公开 API: 函数签名、错误码、幂等语义
持久化: 表字段含义、序列化格式
消息契约: 队列主题、事件 schema
观测性: 日志字段名、指标名

# 步骤 3:选择原子重构操作(按安全评级)
rename → 最安全,一次一个符号,IDE 自动重构
extract → 先提取不删原代码,测试绿后再清理
move → 3 步:加 re-export → 更新 imports → 删 re-export
inline → 确认无副作用、无多处调用后执行

# 步骤 4:补行为保持测试
选择方式:
  a. Jest snapshot: expect(component).toMatchSnapshot()
  b. Golden file: 保存期望输出到 golden-files/*.json,测试对比
原则: 测试覆盖所有公开入口的 happy path + 关键错误路径

# 步骤 5:原子提交(每步一个 commit)
格式: refactor(<scope>): <具体操作>
示例:
  refactor(auth): rename getUserData to fetchUserProfile
  refactor(checkout): extract validateCoupon helper
  refactor(utils): move formatDate to shared/formatting
每个 commit 必须独立通过 CI(npm test && tsc --noEmit)

# 步骤 6:推送前验证
运行完整测试套件: npm test
类型检查: npx tsc --noEmit
比对不变量清单:逐条确认仍满足
查 PR diff:确认没有意外的行为变更(如默认参数改变)

返回技能库 更多技能入口