REFACTORING · 坏味道 / 重构手法

重构速查

从坏味道识别、重构手法到安全网搭建、SOLID 落地,再到技术债的识别与偿还,代码质量与规范方向最常用的 47 条要点一张表收齐,随查随用。

47条要点 8大场景 持续更新

📖 速查表

点击展开各小节

🚩 常见坏味道
坏味道特征危害与重构方向
过长函数 函数超过一屏,内部靠注释划分段落,临时变量满天飞 难理解、难复用,Bug 藏身处 → 提取函数 按意图拆小
过大的类 一个类管太多事,字段与方法数以十计,改谁都绕不开它 职责不清、牵一发动全身 → 提炼类按职责拆分
重复代码 同一段逻辑被复制粘贴到多处,改一处漏一处 修改成本成倍放大 → 提取函数收敛,或上移到父类统一维护
过长参数列 参数超过三四个,或一批同类参数成组出现、顺序易传错 调用方负担重、扩展易出错 → 引入参数对象归拢
发散式变化 一个类因多种不同的原因被反复修改,每次需求都动它 违反单一职责 → 按变化方向把职责拆到不同类 一个类只对一类变化敏感
霰弹式修改 一个小的需求变更要同时改 N 个类,改动像霰弹一样散开 漏改即出 Bug → 搬移函数/字段,把相关逻辑聚拢到一处
依恋情结 函数频繁访问别的类的数据,对"自家"字段反而不感兴趣 内聚性差、职责放错位置 → 把函数搬移到数据所在的类
数据泥团 几个字段、参数或集合元素总是成对成组出现(如经纬度+半径) 散落的关联数据易不一致 → 提炼为独立的领域对象
switch 惊悚现身 同一类型判断的 switch / if-else 散落多处,逻辑各不相同 新增类型要改多处、极易遗漏 → 以多态取代条件表达式 重复的 switch 是信号
🛠️ 重构手法 · 函数与变量
手法动机操作要点
提取函数 一段代码有独立意图、值得起一个名字,需要解释它做什么时 先理清局部变量:被读的作参数,被改的看能否返回;命名用「做什么」而非「怎么做」
内联函数 函数体和名字一样直白,或间接层已无价值、拆得过度时 把函数体并入调用点后删除原函数;与提取函数是一对可逆操作 间接层不是越多越好
提炼变量 复杂表达式需要解释其含义,读代码的人要停下来推演时 把子表达式赋给意图清晰的变量;只计算一次且含义单一时收益最大
引入参数对象 几个参数总是成组传递(数据泥团的参数形态),新增字段要改所有签名 合并为一个对象,相关行为(校验、计算)也可随之迁入,调用方签名更稳定
以查询取代临时变量 临时变量只为存一次计算结果,且妨碍提取函数时 抽成查询函数后,任何作用域都能复用这段计算逻辑 为拆函数铺路
🧩 重构手法 · 结构与多态
手法动机操作要点
搬移函数 函数更常使用另一个类的数据,或调用方集中在那个类里(依恋情结解法) 逐个调用方小步迁移,每步跑测试并提交;旧函数可临时委托过渡
搬移字段 字段被另一个类更频繁地读写,数据放错了位置 先封装字段、统一读写入口,再搬迁并更新所有引用点
以多态取代条件表达式 同一类型判断的 switch 散落多处,新增类型要改多处 先建继承/策略结构,再把各分支逻辑搬进各自的实现类 新增类型只加不改
提炼类 类承担多重职责,一组字段与函数总是一起变化 先建新类并建立双向链接,逐步搬移字段与函数,最后断开临时关联
重构前后对比
从纠缠调用到清晰分层:依赖单向向下,测试独立
🛡️ 重构安全网
实践说明要点
测试先行 重构前先补齐目标代码的单元测试,让每一步改动都有即时反馈 没有测试的重构是盲改;测试不必追求全覆盖,先锁住即将改动的行为 安全网第一层
小步快跑 每次只做一处小重构,立即跑测试验证,再进行下一处 出错立刻能定位到刚改的那一步,回滚代价几乎为零
三次法则 第一次做事、第二次容忍重复、第三次必须重构 不必见重复就抽——过早抽象比重复更难改,用次数兜底 时机参考
IDE 重构 vs 手改 优先使用 IDE 的 Rename / Extract / Inline 等原子重构操作 自动更新全部引用,漏改概率远低于全局手改;手改仅用于 IDE 覆盖不了的结构调整
提交分离 重构提交与功能提交分开,各自独立可评审、可回滚 出问题可精确回退;评审时重构 diff 不混入行为变更 Review 友好
特性开关辅助 大重构用开关灰度切换新旧实现,出问题即时切回 新旧实现并行运行一个观察期,验证后再删除旧路径
📐 SOLID 落地
原则违反示例重构后
SRP 单一职责 User 类既管账号认证又负责发邮件、导报表 按职责拆为 UserService / EmailService / ReportService
OCP 开闭原则 新增一种支付方式就要在核心 switch 里加分支 抽象 PayHandler 接口,新增支付方式只需加一个实现类 扩展不改旧码
LSP 里氏替换 子类覆写父类方法后抛异常或改变约定,父类引用一处通不过 重新审视继承关系,改以组合/委托复用共同行为,而非强转继承
ISP 接口隔离 巨型 Worker 接口逼得实现类写一堆空方法 拆成多个小接口,实现类按需各自实现,不再被无关方法绑架
DIP 依赖倒置 业务层直接 new MySQLDao(),换存储要改业务代码 业务只依赖 Repository 接口,具体实现由注入决定 面向抽象编程
⚖️ 技术债管理
主题说明要点
故意型债务 明知欠债仍赶工期上线("先上线后偿还"),是有意识的风险决策 必须留档(编号、原因、偿还计划)才可控;无记录的"故意"就是裸奔 记下来的债才还得掉
无意型债务 随业务演进、认知加深,代码逐渐腐化而成的债,平时难以察觉 靠 Code Review、静态扫描(SonarQube)与定期重构节奏防止扩散
局部 vs 全局债 单模块内的坏味道 vs 架构级的债(如单体巨石、共享库强耦合) 局部债触碰时随手偿还;全局债影响面大,需单独立项、排期偿还
识别信号 改一处坏三处、需求排期越来越长、新人上手周期明显变长 用工具量化佐证:圈复杂度、重复率、坏味道计数的变化趋势 信号 + 数据双确认
偿还策略 每次触碰该模块顺手做小重构(童子军军规:让营地比来时干净) 高热路径优先偿还、收益最大;把偿还摊进迭代,避免"专门大重构"难以排期
向业务方沟通 不谈术语,用影响、收益、成本三要素换取投入共识 "该模块需求排期 +30%、线上缺陷 40% 集中于此;投入 2 人日重构,预计缺陷 -20%" 用数字换信任
🪜 重构七步节奏
一次安全的重构 = 「小步 + 即时反馈」的循环;七步一个节拍,循环推进直到达成目标
步骤一句话要点提示
① 找到接缝 圈定要动的模块边界,选出下一个最值得改的点 优先「改一处坏三处」的高热文件,收益最大 高热优先
② 补测试 给即将改动的行为补单测,把现状先锁住 测的是「现在」的行为——哪怕现状含 Bug,也先固化再改
③ 一处小改 只做一个原子重构:提取函数、搬移字段、内联… 用 IDE 原子操作;顺手改逻辑 = 破坏节奏
④ 立即跑测试 改完马上跑,红了立刻回退这一步 出错只可能来自刚才那一小步,定位零成本 安全网
⑤ 提交 测试绿了就 commit,小步留档 message 写明重构类型,任何时候都可精确 revert
⑥ 重复 回到第 ③ 步推进下一处,直到目标达成 「小改 → 测试 → 提交」的循环远比一次大改快
⑦ 时机到了再合并 攒齐一个完整意图再发 PR 合入主干 每个提交独立可用,Reviewer 只需看增量 diff
📅 需求里夹带重构的姿势
排期表上永远排不出「专门重构」;把重构摊进日常需求,才是技术债的真实偿还节奏
姿势做法注意点
童子军军规 走过的文件顺手收拾:改个达意的名字、拆掉刚路过的超长函数 只做顺手能完成的小整理,不临时起意发起结构级重构 随手偿还
重构与功能分开提交 同一需求先提纯重构 commit,再提功能 commit(或干脆拆成两个 PR) 混在一起 Reviewer 分不清行为是否变化,出问题也无法单独回滚 禁止混提
每次 PR 重构占比上限 约定重构 diff 占比上限(如 20%~30%),超出部分另立任务排期 防止「顺手」演变成失控大改,反把需求排期拖垮
用特性开关保护 行为有变化的重构挂开关,新旧实现一键切换、线上即时回退 灰度观察一个完整周期后再删旧路径,别让开关常驻 可回退
量化收益 重构前后对比圈复杂度、重复率、坏味道计数,留档佐证 有数据的重构才换得来下一次排期;无记录等于没做过