核心问题
重构的目标是什么?
重构不是为了“代码看起来更漂亮”。
好的重构是为了:
- 降低认知负担
- 恢复概念边界
- 提高局部性
- 恢复一致性
- 移除假抽象
- 让未来变化更自然
核心句:
好重构不是炫技,而是让系统重新变得顺手。
重构不是重写
很多人一说重构,就想推倒重来。
但重写风险很大:
- 旧系统隐性规则会丢失
- 新系统长期没有业务价值
- 新旧系统并行复杂度翻倍
- 迁移成本被低估
- 团队容易陷入永远未完成
真正成熟的重构通常是:
在系统还活着的时候,逐步替换它的骨架。
重构前先固定承诺
重构不应该改变外部行为,除非你明确要改变契约。
所以重构前要先补 characterization tests。
例如重构课程访问逻辑前,先固定:
- 购买后可访问
- 退款后不可访问
- 订阅有效可访问
- 订阅过期不可访问
- 企业分配可访问
- 账号封禁后不可访问
测试不是为了证明旧系统完美,而是为了防止重构时无意改变行为。
核心句:
没有证据保护的重构,容易变成重写。
重构的几种目标
1. 改名:恢复概念边界
改名是低成本高价值重构。
例如:
User -> Account
会立刻暴露哪些字段不属于账号:
billingAddress
courseProgress
organizationRole
instructorBio
再比如:
isActive -> canSignIn
会暴露它和订阅有效、课程上架不是一回事。
改名的目的不是美化,而是让系统承认真实边界。
2. 抽规则:减少重复判断
坏状态:
order.status === "paid" && !order.refundedAt
散落在多个地方。
重构:
isPurchaseEligibleForAccess(purchase)
或者:
canGrantCourseAccessFromPurchase(purchase)
这会把隐式规则变成显式名字。
3. 移动代码:提高局部性
如果访问规则在多个模块,移动到:
course-access/policy.ts
如果退款撤销逻辑散落,移动到:
course-access/revocation.ts
移动代码的目标是:
让变化发生在它应该发生的地方。
4. 拆类型:承认真实差异
坏类型:
type UserCourseRelation = {
accountId: string
courseId: string
type: string
metadata: Record<string, unknown>
}
重构:
Purchase
Enrollment
CourseAssignment
CourseAccessGrant
拆类型不是为了类型多,而是承认不同生命周期和不变量。
5. 删除假抽象
有时候最好的重构是删除抽象。
例如:
UniversalResourceManager
内部全是:
if (resourceType === "course") ...
if (resourceType === "organization") ...
if (resourceType === "payment") ...
这类抽象没有真正统一变化轴。
拆回具体模块,系统反而更清楚。
核心句:
不是所有重构都是抽象,有些重构是去抽象。
小步重构路线
重构最好小步进行。
例如要从混乱 User 模型拆出 Account 和 OrganizationMembership:
1. 给现有 User 增加 Account 语义测试
2. 改名或引入 Account alias
3. 新增 organization_memberships 表
4. 双写 user.companyId 和 organization_memberships
5. 读路径切到 OrganizationMembership
6. 验证一致性
7. 停止写旧字段
8. 删除旧字段
每一步都应该:
- 可测试
- 可 review
- 可回滚或可暂停
- 不要求后续步骤才能工作
重构时保持系统可运行
避免这种计划:
先改所有模型
再改所有调用方
最后一起修测试
这会让系统长时间处于不可运行状态。
更好的方式:
引入新路径
兼容旧路径
逐步迁移调用方
验证一致性
删除旧路径
也就是 expand-contract。
核心句:
好重构允许你中途停下来,系统仍然可用。
重构和审美指标
重构后应该问:
- 名字是否更准确?
- 规则是否更集中?
- 变化是否更局部?
- 错误模型是否更一致?
- 新需求是否更有自然落点?
- 测试是否更清楚?
- 调用者是否知道更少内部细节?
- 删除的代码是否比新增的代码更有价值?
如果重构后只是“更高级”,但调用者更难懂,可能不是好重构。
重构的坏味道
1. 无保护重构
没有测试,没有观测,没有回滚。
2. 顺手改需求
重构时偷偷改变外部行为。
如果要改行为,要明确说这是契约变化。
3. 大爆炸重构
一次改太多,review 和回滚都困难。
4. 抽象成瘾
看到重复就抽,没有找变化轴。
5. 品味压人
只说“这样更优雅”,但说不出工程收益。
好的重构应该能解释它降低了什么成本。
重构检查清单
开始重构前问:
- 我要改善什么?命名、局部性、一致性、组合性,还是可测试性?
- 现有行为有没有测试保护?
- 是否会改变外部契约?
- 能不能小步提交?
- 每一步能不能独立工作?
- 是否需要双写、兼容层或 feature flag?
- 如何验证迁移完成?
- 什么时候删除旧路径?
小结
- 重构的目标是降低认知负担、恢复边界和提高可变性。
- 重构不是重写。
- 重构前先固定系统承诺。
- 改名、抽规则、移动代码、拆类型、删除假抽象都是重构。
- 好重构小步进行,并保持系统可运行。
- 有些重构是抽象,有些重构是去抽象。
- 好重构必须能说清它降低了什么未来成本。