新闻详情

新闻详情

首页 / 资讯中心 / 详情

TDD和领域模型驱动:淘客返利系统从贫血到充血的重构实录

发布时间:2026/9/30 4:51:33来源:尧图网络
TDD和领域模型驱动:淘客返利系统从贫血到充血的重构实录
如果你想对一个已经跑了六年的淘客返利系统做重构心里多少有点发怵。这个系统属于典型的遗留代码订单对象和佣金对象只有 getter 和 setter所有业务逻辑都散落在 Service 层里贫血模型的味道扑面而来。我接手时光是“计算返利”这个动作就在三个地方出现过三份不完全一样的实现改一个 bug另外两处跟着遭殃。后来我们决定做渐进式重构核心思路就是先用 TDD 把旧行为钉死再把贫血模型慢慢改造成充血领域模型让计算逻辑回到模型内部。这篇文章完整记录了这次重构的思路、步骤和踩坑经历适合正在跟遗留代码死磕的中后台开发同学参考。1. 项目背景这个淘客返利系统到底烂在哪1.1 系统的业务与模块分布这个系统做的是电商导购返利用户在站内领取推广链接通过链接去合作电商平台下单平台回传订单数据后系统判断订单是否有效、计算返利佣金、确认收货后把佣金入账到用户余额最后用户发起提现。表面上看链路不复杂但实际落地后细节非常重。订单来源有多个平台每个平台的回传字段格式不同商品佣金比例受类目、活动、用户等级、优惠券使用情况等多重因素影响订单还有部分退款、全额退款、换货、拒收等状态流。这些规则叠加起来代码里出现了大量“先判断状态再判断金额再判断活动类型”的嵌套 if。我接手时核心模块大致有 4 块订单同步模块、返利计算模块、用户资产模块余额、佣金流水、提现、运营配置模块类目佣金、活动规则。前后端是 PHP 时代遗留的老系统后来部分服务用 Java 重写过但重写只覆盖了薄薄一层接口底层那些核心计算逻辑还是老代码。1.2 贫血模型给维护带来的具体痛苦所谓贫血模型简单说就是模型对象只负责装数据不负责行为。Order类的字段有十几个但只有getStatus()、setStatus()、getPayAmount()之类的方法真正的业务规则全部写在外部OrderService里。这种写法最典型的问题有三个。第一个是规则漂移。同一个规则在多个 Service 里各写一份场景还略有差别。比如佣金计算订单同步回调里有一份结算任务里有一份运营后台手工触发里还有一份。三份代码的生效条件不完全一致加了一个“新人专享佣金加成”后只有其中两份同步更新了另一份漏改。线上出现同一笔订单在不同时间点查出来的返利金额不一致排查了两天才定位到是规则分支逻辑分叉。第二个是难以测试。业务逻辑在 Service 的方法里但这个方法又依赖数据库查询、远程接口、缓存等一堆东西。想单独测一个返利计算规则需要构造订单表数据、商品表数据、活动表数据、平台配置表数据再 mock 掉一堆依赖。我见过同事为了测一个佣金计提逻辑光 setUp 就写了三百行。测试成本一高大家就不愿意写测试回归就只能靠人工点页面。第三个是状态判断特别容易漏。贫血模型下业务对象的合法状态全靠 Service 代码约定没有人能回答“一笔已退款的订单状态还是已确认收货应该怎么办”这类问题。因为字段和状态之间没有不变式保护任何一段代码都可以把Order置成你想不到的组合状态。1.3 为什么选择渐进式改造而不是推倒重写当时也有团队同学提议重写系统理由是老代码太烂重写可以彻底按新架构来做。我反对的原因很现实这个系统每天还在跑真实订单和真实资金没法停业务规则散落各处连一个完整可用的“需求文档”都凑不出来资金类系统的正确性要求极高一旦重写过程中规则理解偏差线上资损很难修复。渐进式重构或者说“绞杀者模式”的思路是不破坏系统运行状态一次只切一个边界通过测试保证行为不变逐步把老代码中的逻辑迁移到新模型中。整个改造周期大概持续了四个月前两个月基本只加测试和梳理行为后两个月才开始逐步移动代码。回头来看慢是慢了点但几乎没有出现过线上资金事故这个代价是值得的。2. 先画领域边界再动手改模型2.1 找出核心领域别一上来就建模很多人在充血模型改造上容易犯一个错想一次性把整个系统建出一个完美领域模型结果画了一堆类图代码一行没动。我的建议是先用事件风暴或者简单的“业务动作清单”把系统里的核心概念过一遍搞清楚哪些是核心域、哪些是支撑子域。对淘客返利系统来说核心域一定是“返利计算与结算”支撑子域包括商品同步、订单拉取、会员体系、通知服务等。核心域这部分值得我们把模型做深支撑子域只要能稳定提供接口就好不必强行充血。我当时列了一张表把每个业务动作和它真正依赖的数据放在一起看业务动作涉及核心对象现在逻辑所在地属于哪类域订单回写Order, OrderItem同步 Service支撑域计算返利Order, CommissionPolicy, UserLevel返利 Service核心域确认收货结算Order, Settlement, UserBalance结算 Job核心域提现审核Withdrawal, UserBalance提现 Service支撑域看到这张表后决策就很清晰了重点改造Order、CommissionPolicy、Settlement、UserBalance这几个对象其他对象暂时只做防腐层不深入重构。2.2 聚合边界怎么划别贪大求全领域建模里最容易吵起来的就是聚合边界。返利计算要用到订单、商品、活动规则、用户等级如果把这几样全部塞进一个聚合下模型会非常大事务边界也很难处理。我的处理方式是分两个聚合Order聚合负责订单状态与订单明细相关的行为比如“订单是否可结算”“订单实付金额是多少”“退款金额如何影响佣金基数”CommissionPolicy聚合负责佣金规则相关行为比如“根据类目、用户等级、活动类型计算佣金比例”。计算返利时应用服务从仓库取出这两个聚合在应用层把数据组合起来调用领域服务完成计算。这样划分的好处是订单和规则的生命周期不同混在一起会互相污染。订单聚合关注的是“这笔订单的事实状态”规则聚合关注的是“当前生效的计佣规则”它们之间通过calculate()方法的参数进行协作而不是把规则对象直接挂到订单聚合内部。2.3 分层调整应用服务、领域服务、基础设施改造前的老代码Service 层既是应用服务又是领域服务还是数据访问入口。一个OrderService里既有syncOrderFromPlatform()这种外部对接方法又有calculateRebate()这种核心计算还直接使用OrderMapper查数据库。重构后我把代码明确分成三层应用层只做参数校验、事务控制、调用领域服务不写业务规则。领域层模型对象内部实现业务规则领域服务只处理跨多个对象的协调逻辑。基础设施层仓储接口的实现、远程调用、数据库映射全部向上层隐藏。依赖方向永远从应用层指向领域层从领域层指向基础设施层接口不允许反过来。以前那种领域对象里直接Autowired一个 Mapper 的写法是这个改造中划分红线时最先被禁止的。2.4 从“行为被到处借用”到“行为内聚到模型”改造前Order的状态判断散落在各种 Service 中例如“这个订单是否已确认收货”可能是if (confirmed.equals(order.getStatus()) order.getSettlementStatus() 1)。这三四个字段组合起来才算“可结算订单”。充血模型的第一步就是把这些组合判断收敛到Order内部public class Order { private String status; // 订单状态: paid/confirmed/refunded private int settlementStatus; // 结算状态: 0 未结算 / 1 已结算 private Money payAmount; private Money refundAmount; public boolean canSettle() { return confirmed.equals(status) settlementStatus 0 refundAmount.lessThan(payAmount); } }这个动作看似简单但它把散落的规则统一成了一个模型方法。调用方不再需要知道settlementStatus到底有哪些取值只需要问order.canSettle()。这一步是充血模型的起点模型开始对自己的状态负责。3. TDD 的红色阶段先把旧行为钉死在测试里3.1 特征测试和单元测试怎么配合改遗留代码时最大的心理压力是“改了出问题怎么办”。TDD 的标准流程是红—绿—重构但面对老代码很多人会遇到一个问题旧方法里的逻辑根本不是你写的怎么知道预期结果是什么所以我在重构前会先写特征测试Characterization Test也叫表征测试。这种测试不校验“需求应该是怎样的”而是校验“代码当前实际行为是怎样的”。先把当前行为锁定重构时只要测试还通过就说明行为没变。特征测试通常从外部入口写起覆盖一个完整的分支场景。比如“已确认收货且未结算且金额大于退款金额时订单可结算”这个分支如果我找不到需求文档就先照着现有代码跑一遍结果然后把这个结果写进测试断言里。单元测试则用来抓住内聚后的领域模型行为。等Order.canSettle()提炼出来后再对这个方法写更细粒度的单元测试。两种测试不是替代关系特征测试是安全网单元测试是新模型的规格说明。3.2 怎么快速构造特征测试的数据老代码不好测很大程度是因为数据访问、外部接口全都写在业务代码里。我处理的“计算返利”逻辑入口方法里面直接查询订单、查询商品类目、查询用户等级绕不开数据库。我们的做法是先引入一个薄薄的仓储接口把最常用的几类查询放进接口然后给测试提供内存实现或测试替身。public interface OrderRepository { Order findById(Long orderId); ListOrder findSettledOrders(Long policyId, LocalDate date); }测试里用内存 Map 实现这个仓储就能快速拼装各种场景。这一步本身不算重构业务逻辑但它为后续测试打开了路。如果老的 Service 直接把 SQL 写在方法里我建议先抽 Repository 接口用最少改动换取可测试性。3.3 我踩过的“假红”和“假绿”写特征测试时最要命的是测试环境数据不干净导致你以为自己在验证 A 场景实际数据里混了 B 场景的状态。我踩过一次给订单构造了三笔子订单想测“部分退款”分支结果其中一笔子订单还被另一个测试的事例污染了测试跑出来是绿的但断言的数据根本不是我的用例。排查了半天最后发现是共享测试数据库导致的。后来我定了一条规矩涉及资金计算的测试一律不能连共享数据库全部使用内存仓储 预置 fixture。测试数据要非常精确小数点、状态字段、枚举值都要显式写清楚不要用“随便一条订单”来偷懒。偷懒一时爽排查火葬场。4. 绿色与重构把逻辑一步步搬进领域模型4.1 佣金计算的旧实现长什么样老代码里计算返利的逻辑非常典型一个方法几百行里面全是 if-else 和临时变量。我简化后大概是下面这个样子public BigDecimal calRebate(OrderDO order, ProductDO product, UserDO user) { BigDecimal result BigDecimal.ZERO; if (paid.equals(order.getStatus())) { return result; } if (refunded.equals(order.getStatus())) { return result; } BigDecimal base product.getPrice().multiply(new BigDecimal(0.15)); if (order.getCouponAmount() ! null order.getCouponAmount().compareTo(BigDecimal.ZERO) 0) { base base.multiply(new BigDecimal(0.9)); } if (vip.equals(user.getLevel())) { base base.add(new BigDecimal(1)); } if (order.getRefundAmount() ! null) { base base.subtract(base.multiply(order.getRefundAmount()) .divide(order.getPayAmount(), 2, RoundingMode.HALF_UP)); } return base; }这段代码的问题很明显订单状态判断堆在开头规则判断顺序依赖调用者的书写习惯金额计算没有类型保护BigDecimal直接裸奔。更重要的是这个方法只能处理单一商品真实订单是多商品的还得在外面先循环循环过程中再把订单表、商品表、用户表的数据反复交叉传递。4.2 重构后的领域模型设计我在改造后的模型里把订单和规则分别建模返利计算变成一个领域服务的协作过程public class CommissionCalculator { public Money calculateRebate(Order order, CommissionPolicy policy) { if (!order.canSettle()) { return Money.zero(); } Money base policy.calcRebate(order.getSettledItems()); return policy.applyLevelFactor(base, order.getBuyerLevel()); } }Order里面维护了订单状态、商品明细、支付金额、退款金额并且自己处理“当前订单可不可结算”“实际参与返利的是哪些金额”这些约束。CommissionPolicy负责“不同类目分别给多少比例”“用户等级有什么加成”“优惠券使用后是否打折”这些规则。CommissionCalculator只做编排不把规则写在自己身上。这段代码和旧实现最大的区别是行为有了归属。订单相关的约束不会跑到规则代码里规则相关的调整也不会散落在订单模型里。后续如果再新增一个“平台专项活动”因子只需要改CommissionPolicy不需要动Order。4.3 一次完整的红绿重构过程实录我拿“部分退款后佣金基数应该扣减”这个案例来演示整个 TDD 过程。第一步先写一个红测试Test public void 部分退款后_佣金基数按剩余支付金额计算() { Order order OrderBuilder.create() .withProduct(product, new Money(100)) .withPayAmount(new Money(100)) .withRefundAmount(new Money(30)) .confirmed() .build(); Money result calculator.calculateRebate(order, policy); assertEquals(new Money(7.00), result); }这个测试刚跑的时候必须红因为老代码里没有Order这个对象也没有CommissionPolicy的calcRebate方法。我先用最简单的方式把这个测试写绿哪怕直接把计算结果硬编码回去只要测试过了就可以。第二步写绿。我用最朴素的方式实现calculateRebate明显有重复也没关系绿测试是最优先的。等测试过了再开始第三步重构。第三步重构代码结构。把Money的精度处理、四舍五入、比较大小等逻辑收进Money类把canSettle()的判断收敛进Order把比例查询和等级加成收进CommissionPolicy。每走一步都重新跑一遍测试直到所有特征测试和单元测试都保持绿色。这个过程一开始会感觉很慢因为从一个方法中拆出一个规则可能只需要几分钟但写测试、跑回归、确认行为不变加起来要半个小时。可一旦拆多了后续新需求的开发速度会明显加快。改到第五个规则时我明显感觉内心的确定性提高了因为测试已经覆盖了绝大部分分支再也不用靠人工点页面去猜。4.4 事务边界一定不要在领域模型里操作数据库充血模型改造中最容易翻车的点是把仓储或 Mapper 直接注入领域对象让领域模型自己去查数据。这样看起来模型很“自治”但会导致事务不可控、延迟不可控、测试困难。我的约定是领域对象只处理内存中的状态所有数据加载和落库都发生在应用服务层。应用服务开启事务从仓储取出聚合调用领域行为再把结果交给仓储保存。谁开启事务谁负责持久化这个职责不能下放到模型。public class RebateAppService { Transactional public void settleOrders(SettleCommand cmd) { ListOrder orders orderRepository.findWaitSettleOrders(cmd.getDate()); for (Order order : orders) { CommissionPolicy policy policyRepository.findEffectivePolicy(order.getCategoryIds()); Money rebate calculator.calculateRebate(order, policy); order.markSettled(rebate); orderRepository.save(order); } } }这里的Order内部会有markSettled()方法把状态从“待结算”改到“已结算”但markSettled()不会自己去查数据库它只改自己的内存状态。持久化交给orderRepository.save(order)。5. 踩坑记录重构过程中差点劝退我的那些问题5.1 测试跑得慢构建开始让人崩溃我们把大量逻辑拆出来之后加了不少单元测试但早期的测试里很多还是在起 Spring 容器、连数据库、等远程 mock 调用。跑一轮全量测试要十几分钟开发效率骤降同事开始抱怨“重构完怎么代码比以前还难改”。后来我意识到资金计算领域的核心测试根本不需要 Spring。只要不碰数据库和中间件就用纯 Java 构造对象、调用方法、断言结果。我把核心计算相关测试全部改成纯单元测试跑一轮只需要几秒。只有那些需要验证 SQL 映射、事务回滚的测试才保留在集成测试中单独用慢速标签隔离。这条经验对我非常重要TDD 配合重构时反馈速度决定开发者的习惯。如果红绿循环要等三十秒你就只会想着“少跑几次测试”如果红绿循环只需要三秒你会愿意每改一个条件就测一次。5.2 历史数据兼容与存量订单处理重构过程中还碰上了一个特别棘手的问题老代码里的状态字段不干净。很多历史订单存在“状态是已确认收货但结算状态也是已结算”“退款金额大于支付金额”这类脏数据。如果直接把旧代码改成严格的模型不变式比如在Order构造时就校验refundAmount payAmount那历史数据加载出来直接抛异常线上就炸了。我在领域模型里加了一个兼容层处理构造Order时如果发现脏数据先记录告警再按最保守的行为处理。比如退款金额异常时模型直接判定该订单不可结算同时把原始值暴露给运营后台人工核查。这个策略既保护了模型的不变式也没有让历史数据彻底变成“不可读”状态。5.3 团队协作不是所有人都能立刻接受领域模型这个项目不是一个人能扛下来的团队里既有深耕业务多年的老员工也有刚毕业不久的新同学。老员工熟悉旧代码但对 TDD 和领域模型不太感冒新同学容易倒向“模型设计得越复杂越有成就感”的坑里。我们的做法是每天花一点时间做“重构评审”只评审测试和模型行为不评审编码风格。每完成一个规则内聚就把新旧实现放在一起讲一遍讲清楚“这个行为原来散在哪、现在归属到哪、测试覆盖了哪些分支”。讲了一周之后团队对模型的信任度明显上来了。这里我的体会是重构不只是技术工作更是一次团队记忆的迁移。如果团队不知道某个规则为什么搬进模型下次需求来了还是会绕开模型写新逻辑。5.4 重构进度的度量别看“代码行数”要看“规则覆盖率”前期我们试过用代码行数、类数量来衡量重构进度发现完全没用。拆一个类会增加行数内聚一个行为又减少行数指标看着很混乱。后来换了个更贴近目标的度量方式核心业务规则覆盖率。我把返利计算相关的规则清单列出来比如“退款后佣金基数扣减”“用户等级加成阈值”“活动优惠佣金打折”每完成一条规则的测试和模型内聚就在清单上打勾。改造到第三个月时清单上完成了 30 条核心规则中的 28 条这时线上业务再去加新需求我心里是有底的因为每一处行为变化都会被测试第一时间捕捉到。6. 最后分享一点个人体会这次重构最让我意外的收获不是领域模型多么完美而是团队对“修改代码”这件事的态度发生了变化。以前改一个返利规则大家会先看三个 Service再猜哪份代码生效最后还不敢确认有没有遗漏现在改规则先找测试再改模型方法跑一遍测试就能比较确定地说没问题。如果你也想对类似的系统做重构我建议不要一开始就追求完美的领域模型先花时间把当前行为用测试钉死。测试数量和模型设计都不是目的真正目的是让下一次修改变得安全、快速、可预测。遇到困难的时候记得一次只拆一条规则保持绿灯的时间越长系统就越安全。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

医疗AI落地实战:合规红线、私有化部署与RAG微调全解析 2026/9/30 5:53:25

医疗AI落地实战:合规红线、私有化部署与RAG微调全解析

1. 医疗AI落地的第一道选择题:先搞清楚什么不能碰医疗AI这个方向,我做了快三年,见过太多团队一上来就冲着“大模型诊断”去,结果产品还没出Demo,合规那边就已经亮红灯了。标题里说的“踩红线”,不是危言耸听…

阅读更多 →
Windows 与 Linux 命令对照及终端选型避坑 2026/9/30 5:53:25

Windows 与 Linux 命令对照及终端选型避坑

在 Windows 和 Linux 之间来回切换的人,多少都经历过这种尴尬:在 Linux 上肌肉记忆敲出ls,换到 cmd 里一回车,屏幕冷冷回一句"不是内部或外部命令";反过来在 PowerShell 里习惯了Get-ChildItem,登…

阅读更多 →
Vision Transformer(ViT)原理与实战:从图像切片到全局建模 2026/9/30 5:53:25

Vision Transformer(ViT)原理与实战:从图像切片到全局建模

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
深度学习期末复习:反向传播、卷积计算与优化器归一化 2026/9/30 5:53:25

深度学习期末复习:反向传播、卷积计算与优化器归一化

期末季的图书馆四层,总能看到有人抱着一摞打印的slides,从第一页翻到最后一页,翻完之后长叹一口气——知识点全见过,题一个不会做。深度学习这门课的期末考试就属于这种类型:它不考你背了多少名词,而是把反…

阅读更多 →
计算机为什么用二进制:补码、浮点误差与位运算避坑 2026/9/30 5:53:24

计算机为什么用二进制:补码、浮点误差与位运算避坑

面试季里我最喜欢问的一个问题就是:为什么计算机要用二进制?计算机专业的学生几乎人人能背出"电路只有通和断两种状态",但再往下追问一句——为什么不是三进制、不是十进制?——能答完整的人就少了一大半。二进制这个词…

阅读更多 →
从《云计算导论》到实战:IaaS、PaaS、SaaS 与运维避坑指南 2026/9/30 5:53:11

从《云计算导论》到实战:IaaS、PaaS、SaaS 与运维避坑指南

简介:这份《云计算导论》文档面向计算机专业学生、IT从业者及希望系统了解云计算基础概念的学习者,帮助读者从零建立对云计算定义、技术原理与产业影响的整体认知。资源为单个doc文档,压缩包约61KB,内容以章节化讲义形式组织&…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉