使用DDD优化转账功能——从混乱到清晰

📅 发布时间:2026/7/10 10:19:38 👁️ 浏览次数:
使用DDD优化转账功能——从混乱到清晰
一、引言在上一篇文章中我们讨论了系统是如何逐渐“老化”的——贫血模型、事务脚本、大泥球架构。但到底怎么用DDD改造代码现在我们就通过一个最经典的转账功能手把手演示如何将传统MVC代码逐步重构为DDD四层架构。这个实战会让你直观地理解DDD中的核心战术构件实体、值对象、仓库、防腐层、领域服务。二、原始转账功能典型的“事务脚本”风格2.1 业务需求与产品设计业务需求用户购买商品后向商家进行支付。产品设计的实现步骤拆解为从数据库中查出用户和商户的账户信息调用风控系统的微服务进行风险评估实现转入、转出操作计算双方的金额变化保存到数据库发送交易情况给Kafka用于后续审计和风控。2.2 开发MVC 随手就来一个典型的ControllerServiceDAO实现javapublic class PaymentController { private PayService payService; public Result pay(String merchantAccount, BigDecimal amount) { Long userId (Long) session.getAttribute(userId); return payService.pay(userId, merchantAccount, amount); } } public class PayServiceImpl extends PayService { private AccountDao accountDao; // 操作数据库 private KafkaTemplateString, String kafkaTemplate; // 操作Kafka private RiskCheckService riskCheckService; // 风控微服务接口 public Result pay(Long userId, String merchantAccount, BigDecimal amount) { // 1. 从数据库读取数据 AccountDO clientDO accountDAO.selectByUserId(userId); AccountDO merchantDO accountDAO.selectByAccountNumber(merchantAccount); // 2. 业务参数校验 if (amount clientDO.getAvailable()) { throw new NoMoneyException(); } // 3. 调用风控微服务 RiskCode riskCode riskCheckService.checkPayment(...); // 4. 检查交易合法性 if (!0000.equals(riskCode)) { throw new InvalidOperException(); } // 5. 计算新值更新字段 BigDecimal newSource clientDO.getAvailable().subtract(amount); BigDecimal newTarget merchantDO.getAvailable().add(amount); clientDO.setAvailable(newSource); merchantDO.setAvailable(newTarget); // 6. 更新到数据库 accountDAO.update(clientDO); accountDAO.update(merchantDO); // 7. 发送审计消息 String message userId , merchantAccount , amount; kafkaTemplate.send(TOPIC_AUDIT_LOG, message); return Result.SUCCESS; } }2.3 这段代码有什么问题代码层面和系统层面的多重问题维度问题单一职责原则PayService承担了数据校验、风控、计算、持久化、消息发送等所有职责依赖反转原则高层业务直接依赖低层DAO和第三方中间件无法单元测试开放封闭原则增加新校验或审计逻辑必须修改核心服务类贫血模型AccountDO只有getter/setter业务逻辑散落在Service中可测试性无法脱离数据库、Kafka、风控服务进行单元测试整体代码流程图清晰地展示了所有步骤串行耦合在一起任何一个环节变更都可能引起全盘回归测试。三、动手改造第一步抽象数据存储层3.1 引入实体、值对象告别贫血模型首先我们将AccountDO升级为领域实体Account并将金额抽像为值对象Moneyjavapublic class Account { private Long id; private AccountNumber accountNumber; // 值对象 private Money available; // 值对象 // 转入商户收到钱 public void withdraw(Money money) { available available.add(money); } // 转出用户扣钱 public void deposit(Money money) { if (available.compareTo(money) 0) { throw new InsufficientMoneyException(); } available available.subtract(money); } } // 值对象账户号 public class AccountNumber { private final Long value; // 构造、equals、hashCode ... } // 值对象金额 public class Money { private final BigDecimal amount; // 构造、加减比较、equals、hashCode ... }3.2 引入 Repository隔离数据源定义AccountRepository接口将数据持久化彻底隔离javapublic interface AccountRepository { Account find(AccountNumber accountNumber); Account find(UserId userId); Account save(Account account); } Repository public class AccountRepositoryImpl implements AccountRepository { Autowired private AccountDao accountDAO; Autowired private AccountBuilder accountBuilder; Override public Account find(AccountNumber accountNumber) { AccountDO accountDO accountDAO.selectByAccountNumber(accountNumber.getValue()); return accountBuilder.toAccount(accountDO); } Override public Account save(Account account) { AccountDO accountDO accountBuilder.fromAccount(account); if (accountDO.getId() null) { accountDAO.insert(accountDO); } else { accountDAO.update(accountDO); } return accountBuilder.toAccount(accountDO); } }AccountBuilder负责Account↔AccountDO的转换封装了DAO与实体之间的映射细节。四、动手改造第二步抽象第三方服务——防腐层外部风控接口是我们的依赖但我们不希望领域层直接依赖它。DDD给出的解是防腐层Anti-Corruption LayerACL——在领域与外部系统之间构建一层翻译隔离javapublic interface BusiSafeService { Result checkBusi(Account client, Account merchant, Money money); } Service public class BusiSafeServiceImpl implements BusiSafeService { Autowired private RiskChkService riskChkService; // 外部RPC客户端 public Result checkBusi(Account client, Account merchant, Money money) { // 参数封装、调用外部服务 RiskCode riskCode riskChkService.checkPayment(...); if (0000.equals(riskCode.getCode())) { return Result.SUCCESS; } return Result.REJECT; } }现在领域层只需面向BusiSafeService接口编程外部风控系统的变化被完全隔离在防腐层内部。五、动手改造第三步抽象中间件——同样用防腐层Kafka 是技术组件不属于领域。我们同样通过防腐层将其隔离javapublic class AuditMessage { private UserId userId; private AccountNumber clientAccount; private AccountNumber merchantAccount; private Money money; private Date date; // 构造、getBody() ... } public interface AuditMessageProducer { SendResult send(AuditMessage message); } Service public class AuditMessageProducerImpl implements AuditMessageProducer { private KafkaTemplateString, String kafkaTemplate; Override public SendResult send(AuditMessage message) { String messageBody message.getBody(); kafkaTemplate.send(audit-log, messageBody); return SendResult.SUCCESS; } }领域层完全不知道KafkaTemplate的存在它只依赖AuditMessageProducer接口。六、动手改造第四步用领域服务封装多实体逻辑转账涉及两个实体用户账户、商户账户的状态变更属于跨实体的业务逻辑不适合放在任一实体内部。DDD通过领域服务来表达这种无状态的协调逻辑javapublic interface AccountTransferService { void transfer(Account sourceAccount, Account targetAccount, Money money); } Service public class AccountTransferServiceImpl implements AccountTransferService { Override public void transfer(Account sourceAccount, Account targetAccount, Money money) { sourceAccount.deposit(money); // 用户扣钱 targetAccount.withdraw(money); // 商户加钱 } }领域服务非常薄只负责编排实体方法不包含具体业务规则规则都在实体自身。七、重新编排DDD四层架构下的转账服务现在我们将所有重构好的组件按照DDD四层架构重新组装。7.1 四层架构示意图PPT第13页对比了传统MVC架构与DDD四层架构传统MVCController → Service → DAO → DB所有业务逻辑堆积在Service层基础设施直接暴露给业务。DDD四层用户接口层Controller应用层Application Service非常薄协调领域对象领域层实体、值对象、领域服务、仓库接口基础设施层Repository实现、防腐层实现、DAO、Kafka客户端等7.2 重构后的PayServicejavaService public class PayServiceImpl { private AccountRepository accountRepository; private AuditMessageProducer auditMessageProducer; private BusiSafeService busiSafeService; private AccountTransferService accountTransferService; public Result pay(Long userId, String merchantAccount, BigDecimal amount) { // 参数校验转换为值对象 Money money new Money(amount); UserId clientId new UserId(userId); AccountNumber merchantNumber new AccountNumber(merchantAccount); // 1. 加载聚合根 Account clientAccount accountRepository.find(clientId); Account merAccount accountRepository.find(merchantNumber); // 2. 交易检查防腐层调用 Result preCheck busiSafeService.checkBusi(clientAccount, merAccount, money); if (preCheck ! Result.SUCCESS) { return Result.REJECT; } // 3. 执行转账领域服务 accountTransferService.transfer(clientAccount, merAccount, money); // 4. 保存聚合仓库 accountRepository.save(clientAccount); accountRepository.save(merAccount); // 5. 发送审计消息防腐层 AuditMessage message new AuditMessage(clientAccount, merAccount, money); auditMessageProducer.send(message); return Result.SUCCESS; } }核心变化应用层PayService极度精简只做任务协调没有任何业务逻辑所有业务规则被封装在实体、值对象、领域服务中所有基础设施依赖都被抽象为接口由防腐层/仓库实现单元测试可以用 Mock 替换所有基础设施单独测试领域层。八、章节小结通过转账功能的逐步重构我们以代码推演的方式将DDD中那些抽象的概念具象化了DDD概念代码落地实体EntityAccount拥有唯一标识包含业务方法值对象Value ObjectMoney、AccountNumber、UserId不可变通过构造创建仓库RepositoryAccountRepository封装数据持久化领域层只依赖接口防腐层Anti-Corruption LayerBusiSafeService、AuditMessageProducer隔离外部系统和中间件领域服务Domain ServiceAccountTransferService协调多个实体应用层Application ServicePayServiceImpl只做任务编排无业务逻辑但是这种代码层面的推演毕竟太过具体缺乏方法层面的指导意义。DDD的作用远不止指导代码设计这么简单。接下来我们需要从更高的高度、更广的业务视角来理解DDD到底是如何指导我们的程序设计的。