学完这篇,你能把一段几百行、嵌套五六层、谁都不敢动的 Java 业务代码,按可回滚的小步骤拆成职责清晰的整洁结构,并且每一步都有测试兜底。
下面用一个真实场景贯穿:OrderService.calc(),一个 300 行、包含价格计算、优惠券、会员折扣、库存校验、发短信、写日志的方法。别急着删代码,按顺序来。
第一步:先给面条代码拍张「照片」
重构的前提是「行为不变」,所以在动刀之前先固化行为。写测试,别追求覆盖率,只测你能想到的分支。
@Test
void 会员用券满减后不应低于零元() {
Order order = OrderFixture.memberWithCoupon(1000, 1500);
assertEquals(0, new OrderService().calc(order).getPayAmount());
}
注意:如果这段代码依赖数据库、Redis、短信 SDK,先抽接口或加测试替身(Mock)。实在拆不动,就用录制真实请求参数 + 返回值做「黄金样本测试」,先把输入输出钉死。
跑一遍:
mvn -q test
红了就先别往下走。
第二步:把巨型方法切成小块
在 IntelliJ IDEA 里选中一段逻辑,按 Ctrl+Alt+M(macOS 是 Cmd+Opt+M)提取方法,Ctrl+Alt+V 提取变量,Shift+F6 重命名。名字要能直接读出来:calcMemberDiscount 而不是 doCalc2。
一个简单的判断标准:一个方法如果不能在屏幕一屏内看完,或者需要用「第一段、第二段」来描述,就该切。
每次只切一块,切完立刻 mvn test。切了十几次之后,calc() 应该只剩下 20 行左右的编排代码。
第三步:用卫语句削平嵌套
嵌套是面条味的根源。把这种情况:
if (order != null) {
if (order.getItems() != null) {
if (!order.getItems().isEmpty()) {
// 真正的业务
}
}
}
改成早返回:
if (order == null || CollectionUtils.isEmpty(order.getItems())) {
throw new BizException("订单或明细为空");
}
// 真正的业务,缩进归零
注意:改早返回要确认原逻辑里没有「else 分支做别的事」。有的话先拆成两个方法再改,别顺手把行为改了。
第四步:用策略替换条件分支
价格计算里常见一堆 if (type == 1) ... else if (type == 2)。这类分支继续加下去只会越来越长。把它变成多态:
public interface DiscountStrategy {
boolean supports(Order order);
BigDecimal apply(Order order, BigDecimal amount);
}
然后每个分支一个实现类,注册到一个 List<DiscountStrategy> 或 Map<String, DiscountStrategy> 里,用 Spring 的 @Component 直接注入集合即可:
@Service
public class DiscountEngine {
private final List<DiscountStrategy> strategies;
public DiscountEngine(List<DiscountStrategy> strategies) { this.strategies = strategies; }
public BigDecimal calc(Order order, BigDecimal amount) {
return strategies.stream()
.filter(s -> s.supports(order))
.reduce(amount, (acc, s) -> s.apply(order, acc), (a, b) -> b);
}
}
注意:策略顺序会影响结果(满减和打折的先后不一样)。用 @Order 注解或列表装配顺序显式固定,别依赖类名扫描顺序。
第五步:按职责分层,把依赖倒过来
把 calc() 里的写库、发短信、加日志全部挪出去,OrderService 只负责编排:
controller(接参、校验、返回)
↓
OrderService(业务编排,纯逻辑)
↓
DiscountEngine / StockChecker(领域规则)
↓
OrderRepository(数据访问,接口 + 实现)
↓
SmsSender / EventPublisher(外部副作用,接口)
关键动作是把 new SmsClient(...) 改成构造函数注入接口。这样业务逻辑就变成了纯函数式的可测代码,副作用在边界上。
第六步:收口参数和返回值
当一个方法有 7 个参数、其中 3 个是 String 时,早晚会传错。抽出 CalcContext 这样的参数对象,用构造器或静态工厂收口。同理,返回 Map<String, Object> 的地方改成明确的 CalcResult。
注意:改签名会波及调用方。IDEA 的 Shift+F6 重命名和 Ctrl+Alt+Shift+T 重构菜单里有「Change Signature」,能自动更新所有调用点,别手工搜替换。
第七步:小步提交,随时可回滚
每完成一个动作就提交一次:
git add -p
git commit -m "refactor: 提取会员折扣计算,行为不变"
一个提交只做一件事,信息里写清「行为不变」。真出问题时 git bisect 能精准定位到哪一步搞坏了。
小结
- 重构的第一步永远是测试,不是删代码;没有行为基线就别动手
- 用 IDE 的
Ctrl+Alt+M 提取方法、Shift+F6 重命名、Ctrl+Alt+Shift+T 重构菜单,不要手工复制粘贴
- 嵌套用卫语句和早返回削平,条件分支用策略 + 多态替换
- 按 controller → service → domain → repository 分层,外部副作用抽接口从构造函数注入
- 多参数收敛成参数对象,返回
Map 收敛成明确的 DTO
- 每步跑
mvn test,小步提交,提交信息写「行为不变」