老系统重构要把新旧逻辑并行验证
老系统重构要把新旧逻辑并行验证
1. 重构现场:粗暴替换遗留代码引发的生产事故
在一次针对遗留计费系统的重构中,团队试图将一个堆积了 3000 多行、嵌套了 15 层if-else的老方法calculateFee()一口气重构掉。开发人员设计了一套极其优雅的策略模式(Strategy Pattern)与工厂模式,删掉了原有的老代码,将十几种计费规则解耦到了独立的策略类中。
然而新版本一上线,催缴告警群瞬间炸锅:几种组合优惠券在特定生鲜品类下的计费金额算错了 0.5 元,导致大量订单结算失败。由于老代码已经被直接删掉,团队无法快速退回到原有的硬编码逻辑,只能被迫紧急扣压流量进行回滚。
重构老旧遗留系统就像在飞行中更换发动机。绝不能凭主观意志“一次性删除老代码”。应结合设计模式中的策略模式(Strategy Pattern)、适配器模式(Adapter Pattern)以及架构层的“绞杀者模式(Strangler Fig Pattern)”,实现新老代码的渐进式替换与分阶段平滑演进。
[ERROR] 2026-08-27 15:45:10.120 [http-nio-8080-exec-19] c.e.billing.service.StranglerBillingProxy - Fee mismatch between legacy and new strategy! Request: [userId=90122, cartId=C-8812] Legacy Computed Fee: 88.50 New Strategy Computed Fee: 89.00 Diff: +0.50 | Action: Falling back to Legacy Execution to protect billing safety.2. 设计模式驱动的迁移架构:绞杀者与策略模式重构模型
通过设计模式将新老逻辑隔离,构建一个包含旁路双跑与渐进绞杀的平滑演进路径。
模式一:绞杀者模式(Strangler Fig Pattern)作为入口代理
在遗留代码外层包裹一层代理对象(Proxy)。外部客户端完全感知不到内部的重构过程。代理对象负责根据配置开关(Apollo / Nacos)控制流量走老的臃肿逻辑还是新的策略模式逻辑。
模式二:策略模式(Strategy Pattern)解耦复杂业务规则
将老的if-else分支抽取为实现了统一接口的FeeStrategy独立类,借助 Spring 的自动注入特性将策略注册到 Map 容器中,彻底清空老方法中的臃肿分支。
模式三:适配器模式(Adapter Pattern)兼容新老数据模型
遗留老代码依赖的数据模型往往包含大量的历史包袱字段。通过适配器模式,将新策略模式所需的干净 Domain Model 与老系统的 DTO 进行隔离转换,防止老代码的坏味道侵蚀新架构。
3. 生产级重构代码:策略模式 + 绞杀者代理 + Dynamic 路由表
以下代码展示了如何利用 Spring 容器特性优雅实现策略模式,并结合绞杀者代理进行安全切流。
统一策略接口与具体策略实现
package com.example.billing.strategy; import java.math.BigDecimal; public interface FeeStrategy { /** * 获取策略支持的业务类型标识 */ String getSupportedType(); /** * 计算费用 */ BigDecimal calculate(BillingContext context); }package com.example.billing.strategy.impl; import com.example.billing.strategy.BillingContext; import com.example.billing.strategy.FeeStrategy; import org.springframework.stereotype.Component; import java.math.BigDecimal; @Component public class ComboDiscountStrategy implements FeeStrategy { @Override public String getSupportedType() { return "COMBO_DISCOUNT"; } @Override public BigDecimal calculate(BillingContext context) { // 新重构的干净策略逻辑:组合优惠扣减 BigDecimal base = context.getOriginalPrice(); BigDecimal discount = context.getDiscountAmount(); return base.subtract(discount).max(BigDecimal.ZERO); } }策略工厂与绞杀者代理控制
package com.example.billing.proxy; import com.example.billing.legacy.LegacyFeeCalculator; import com.example.billing.strategy.BillingContext; import com.example.billing.strategy.FeeStrategy; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Service public class StranglerBillingProxy { private final LegacyFeeCalculator legacyFeeCalculator; private final Map<String, FeeStrategy> strategyMap = new ConcurrentHashMap<>(); // 假设此开关由配置中心(Nacos/Apollo)动态推送 private boolean dualRunEnabled = true; private boolean fullyMigrated = false; public StranglerBillingProxy(LegacyFeeCalculator legacyFeeCalculator, List<FeeStrategy> strategies) { this.legacyFeeCalculator = legacyFeeCalculator; // 自动将所有 Spring 容器中的 FeeStrategy 注册到 Map 策略路由表中 strategies.forEach(s -> strategyMap.put(s.getSupportedType(), s)); } public BigDecimal calculateFee(BillingContext context) { String type = context.getBusinessType(); // 1. 如果已完全迁移且关闭双跑,直接走新的策略模式 if (fullyMigrated && !dualRunEnabled) { FeeStrategy strategy = strategyMap.get(type); if (strategy != null) { return strategy.calculate(context); } } // 2. 双跑阶段 (Dual-Run Mode):同时运行新老代码并进行结果比对 BigDecimal legacyResult = legacyFeeCalculator.calculateLegacy(context); if (dualRunEnabled) { FeeStrategy strategy = strategyMap.get(type); if (strategy != null) { try { BigDecimal newResult = strategy.calculate(context); // 安全防护:比对新老结算结果,如果不一致,记录日志并依然以老逻辑为准 if (legacyResult.compareTo(newResult) != 0) { System.err.printf("[MISMATCH ALERT] BusinessType: %s | Legacy: %s | New: %s%n", type, legacyResult, newResult); } } catch (Exception e) { System.err.println("New Strategy execution failed: " + e.getMessage()); } } } // 兜底返回老代码结果,确保线上业务百分之百安全 return legacyResult; } }4. 重构过程中的单元测试与新老双跑断言
在平滑演进期间,利用自动化测试脚本进行大规模数据样本的新老比对。
编写对齐校验基准测试类:
package com.example.billing; import com.example.billing.proxy.StranglerBillingProxy; import com.example.billing.strategy.BillingContext; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest public class BillingMigrationTest { @Autowired private StranglerBillingProxy proxy; @Test void testLegacyAndNewStrategyParity() { BillingContext context = new BillingContext(); context.setBusinessType("COMBO_DISCOUNT"); context.setOriginalPrice(new BigDecimal("100.00")); context.setDiscountAmount(new BigDecimal("15.00")); BigDecimal fee = proxy.calculateFee(context); assertEquals(new BigDecimal("85.00"), fee); } }监控后台的旁路 Diff 校验统计:
2026-08-27 16:00:00 [MigrationWorker] INFO c.e.b.p.StranglerBillingProxy - Total Dual-Run Sample: 100000 | Match Count: 100000 | Diff Count: 0 2026-08-27 16:00:00 [MigrationWorker] INFO c.e.b.p.StranglerBillingProxy - Parity reached 100%. Ready to flip `fullyMigrated` feature flag to true.通过这套双跑对比机制,团队在连续一周没有发现任何比对 Diff 后,将配置开关翻转,成功将老臃肿逻辑彻底关停并移除。
5. 设计模式重构避坑守则
- 不应要直接删掉遗留系统的老代码,应使用绞杀者代理(Proxy)将新老实现同时封装在统一接口下。
- 充分利用策略模式(Strategy)拆解大体积
if-else,结合 Spring 依赖注入自动构建策略路由映射表。 - 应建立双跑比对(Dual-Run)机制,只有当新老逻辑对海量生产数据样本的运算结果达到 100% 一致后,才能正式下线老代码。
