news 2026/8/27 17:19:55

老系统重构要把新旧逻辑并行验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
老系统重构要把新旧逻辑并行验证

老系统重构要把新旧逻辑并行验证

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. 设计模式重构避坑守则

  1. 不应要直接删掉遗留系统的老代码,应使用绞杀者代理(Proxy)将新老实现同时封装在统一接口下。
  2. 充分利用策略模式(Strategy)拆解大体积if-else,结合 Spring 依赖注入自动构建策略路由映射表。
  3. 应建立双跑比对(Dual-Run)机制,只有当新老逻辑对海量生产数据样本的运算结果达到 100% 一致后,才能正式下线老代码。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/27 17:14:59

make-sense:免费在线图片标注,从上传到导出只要 5 分钟

make-sense&#xff1a;免费在线图片标注&#xff0c;从上传到导出只要 5 分钟 【免费下载链接】make-sense Free to use online tool for labelling photos. https://makesense.ai 项目地址: https://gitcode.com/gh_mirrors/ma/make-sense 手头攒了几百张照片要标给模…

作者头像 李华
网站建设 2026/8/27 16:58:01

开源项目维护与社区运营要点

开源项目维护与社区运营要点 开源项目维护的核心是降低协作成本。贡献者需要知道项目接受什么问题、如何复现、谁会处理&#xff0c;以及何时可能得到答复。 让入口清楚 问题模板分别收集缺陷复现、功能建议和安全报告&#xff0c;避免敏感信息进入公开讨论。提交请求说明变更…

作者头像 李华