看着PR里那段复制粘贴了三次的代码,我第一反应不是"写的人水平不行",而是"当时我大概也会这么干"。三个方法做的事情几乎一样:从缓存拿数据,拿不到就查数据库,查到了顺手刷新过期时间,最后拼成响应对象返回。区别只是入参类型不一样,以及中间那行查询语句换了张表。这种代码在业务系统里太常见了,常见到所有人都默认它就该长这样。
但"默认"不代表"正确"。方法提取(Extract Method)是重构里最基础、也最容易被用偏的一个手法。很多人以为它就是把重复代码搬进一个新方法,改几个参数名,完事。真这么做,大概率会造出一个更别扭的抽象层——比如那个被塞了六个参数、里面全是if分支的"万能方法"。所以这篇不打算只讲"怎么提取",重点讲提取前怎么判断该不该提取、提取时变化点怎么找、提取后怎么证明没改坏。这些都是我在真实项目里踩过的坑,写出来给正准备动手的你参考。
1. 动手之前,先看清重复逻辑的三种形态
方法提取的第一步不是打开IDE按快捷键,而是先确认你面对的重复到底属于哪种形态。不同形态的重复,处理方式完全不同。我见过太多人栽在第二步:没分清重复类型,强行提取,最后代码比原来还难维护。
1.1 复制粘贴型重复:最容易被识别,也最不值得骄傲
复制粘贴型重复最好认:两段代码几乎一模一样,只差类型名、变量名,或者某个字段路径。比如在一个订单系统里,loadProductData 和 loadOrderData 两个方法,前半段都是先查缓存、缓存没有就查数据库、查完再写回缓存,代码片段几乎完全对齐。
这种重复最让人头疼的地方在于:它不只是丑,它还会放大bug。假设缓存那边要加一个空值保护,或者要补充一段埋点日志,你需要在三个地方同步修改。只要漏改一处,线上就会出现"某个接口有缓存保护、另一个接口还是老逻辑"的诡异差异。所以遇到这种重复,别犹豫,该提取就提取。
1.2 结构一致但细节漂移的"伪重复":方法提取最常翻车的地方
比复制粘贴型更阴险的是"伪重复"。表面上看几个方法都长一个骨架:查出列表、按条件过滤、排序、转DTO、返回。但具体到每一步,过滤条件不同、排序字段不同、DTO的映射规则也不同。
这类代码如果硬提取,很容易掉进"万能参数"陷阱。我以前见过一个方法,为了合并三个"看起来一样"的查询逻辑,塞了五个参数进去,其中有两个是布尔开关,方法内部全是 if (xxx) {} else {}。调用方的代码确实从三段变成三行了,但可读性直接从"啰嗦"变成了"黑盒"——没人知道传 true 和传 false 到底会触发哪条路径。
伪重复的正确处理方式不是"用参数强行统一",而是先看骨架是否真的同构。如果一段代码里有50%相同、50%不同,那把相同的部分(比如分页处理、异常包装)提出来,不同的部分保留在各自方法里,往往比强行合并更合理。方法提取的粒度,一对一错,效果天差地别。
1.3 判断"该不该提取"的三个问题与一张对照表
在按下重构快捷键之前,我自己习惯先过三个问题:
- 这段代码承担的职责是同一个吗?如果两段代码只是"恰好长得像",但业务含义完全不同,提取后会导致语义错位。
- 变化点能否用参数化解决,且不引入复杂分支?判断标准是:参数传入后,方法体内不需要靠
if (type == A)这种开关来分流。一旦需要,说明你提取的根本不是同一种逻辑,只是同一个模板。 - 提取出来的方法名能不能准确表达业务意图?一个叫
processData的方法大概率是抽象过度了;但如果它能叫publishOrderEvent,说明你找到了真正的业务概念。
下面这张表是我平时判断时参考的:
| 重复类型 | 典型特征 | 提取策略 |
|---|---|---|
| 复制粘贴型 | 代码逐行对齐,仅类型/变量不同 | 放心提取,提取后方法语义清晰 |
| 结构同构型 | 骨架一致,细节漂移,但变化点可用参数表达 | 提取稳定骨架,变化点用参数或函数式接口传入 |
| 表面相似型 | 骨架像但职责不同,强行统一需要加开关 | 不要提取,最多抽公共小工具 |
| 超大冗长方法 | 方法本身几百行,内部多个可独立变化的段落 | 优先按语义拆段,不是先消重 |
这个表不严谨,但很实用。它的核心意思是:方法提取的第一目标不是"消除重复",而是"让代码的职责边界清晰"。重复只是你发现职责边界的一个线索,不能当作唯一标准。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 一次完整的方法提取流程:从缓存逻辑重复案例说起
光讲理论容易飘,下面用一个完整的案例走一遍提取流程。这个案例来自一个典型的电商后台服务,逻辑不复杂,但很能说明问题。
2.1 重构前的样子:两个看似不同、实则同构的方法
当时项目里有一个库存查询服务,里面两个方法,分别加载商品信息和订单信息:
java复制public ProductVO loadProductDa
