如何在不依赖基础设施的前提下实现DDD富模型并留存业务逻辑?
在DDD中实现富模型:处理外部依赖与保持领域纯粹的策略
在复杂系统中实现富模型的核心挑战,在于如何让领域逻辑内聚在实体/值对象中,同时合理处理对数据库或外部服务的依赖。以下是务实的落地策略和平衡方法:
一、依赖倒置+端口-适配器模式:隔离领域与外部依赖
核心思路是让领域模型依赖抽象端口,而非具体的外部实现,把依赖的具体实现移到基础设施层的适配器中,确保领域层不被外部细节污染。
比如订单实体需要检查库存才能添加订单项:
// 领域层定义抽象端口(不涉及任何外部实现细节) public interface InventoryChecker { boolean hasSufficientStock(Sku sku, int quantity); } // 富模型订单实体:核心业务逻辑内聚 public class Order { private List<OrderLine> lines; private final InventoryChecker inventoryChecker; // 通过构造注入抽象依赖,避免硬编码外部服务 public Order(InventoryChecker inventoryChecker) { this.inventoryChecker = inventoryChecker; } public void addOrderLine(Sku sku, int quantity) { // 核心业务规则留在领域内:添加订单项前必须校验库存 if (!inventoryChecker.hasSufficientStock(sku, quantity)) { throw new InsufficientStockException(sku, quantity); } this.lines.add(new OrderLine(sku, quantity)); } }
基础设施层实现InventoryChecker接口,比如调用数据库查询库存或外部仓储服务,领域层完全不需要关心这些细节。
二、合理使用领域服务:承载跨实体/依赖外部的领域规则
不要把所有依赖外部的逻辑都丢到应用服务,而是用领域服务封装那些跨实体、或需要外部数据但仍属于领域规则的逻辑:
- 领域服务属于领域层,只处理领域内的规则,不负责流程编排;
- 应用服务负责调用领域服务、协调外部系统,处理非领域的流程逻辑。
比如计算订单折扣需要参考用户会员等级:
// 领域层抽象仓库 public interface MemberRepository { Member findById(MemberId memberId); } // 领域服务:封装折扣计算的领域规则 public class OrderDiscountService { private final MemberRepository memberRepository; public OrderDiscountService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } public BigDecimal calculateDiscount(Order order) { Member member = memberRepository.findById(order.getMemberId()); // 核心折扣规则:会员等级对应不同折扣率 return switch (member.getLevel()) { case VIP -> new BigDecimal("0.8"); case GOLD -> new BigDecimal("0.9"); default -> BigDecimal.ONE; }; } }
这里折扣计算的规则属于领域逻辑,留在领域服务中;会员数据的获取依赖抽象仓库,具体实现由基础设施层完成。
三、值对象内聚逻辑:通过参数传递外部数据
对于不需要持有外部依赖的业务逻辑,优先封装到值对象中,外部数据通过方法参数传入,避免值对象直接依赖外部服务。
比如货币转换逻辑:
public class Money { private final BigDecimal amount; private final Currency currency; public Money(BigDecimal amount, Currency currency) { this.amount = amount; this.currency = currency; } // 转换逻辑内聚在值对象中,汇率作为参数传入 public Money convertTo(Currency targetCurrency, BigDecimal exchangeRate) { BigDecimal convertedAmount = amount.multiply(exchangeRate); return new Money(convertedAmount.setScale(2, RoundingMode.HALF_UP), targetCurrency); } }
汇率的获取由上层(领域服务或应用服务)负责,值对象只专注于货币转换的核心逻辑,保持纯粹性。
四、预加载聚合内数据:让实体可独立完成逻辑
对于聚合内部的关联数据,在加载聚合根时通过仓库预加载,让实体在内存中可以独立执行大部分业务逻辑,减少对外部依赖的实时调用。
比如加载订单时,同时预加载关联的会员等级信息:
// 领域层仓库接口 public interface OrderRepository { Order findByIdWithMember(OrderId orderId); } // 订单实体使用预加载的会员数据计算折扣 public class Order { private Member member; private List<OrderLine> lines; public BigDecimal getDiscountAmount() { // 直接使用预加载的会员数据,无需实时调用外部服务 return switch (member.getLevel()) { case VIP -> getTotalAmount().multiply(new BigDecimal("0.2")); case GOLD -> getTotalAmount().multiply(new BigDecimal("0.1")); default -> BigDecimal.ZERO; }; } private BigDecimal getTotalAmount() { return lines.stream() .map(line -> line.getPrice().multiply(new BigDecimal(line.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); } }
注意预加载的数据必须在聚合边界内,避免加载过多无关数据导致性能问题。
平衡业务逻辑的关键:区分核心与支撑逻辑
要避免业务规则过度迁移到服务层,需明确划分:
- 核心领域逻辑:比如订单状态流转规则、折扣计算、库存校验等,必须留在领域模型(实体、值对象、领域服务)中,这是DDD保持领域纯粹性的核心;
- 支撑逻辑:比如调用支付接口、发送通知、日志记录等,属于应用层或基础设施层的职责,不应侵入领域层。
定期做代码审查和重构,把不小心漂移到服务层的核心逻辑迁回领域模型,保持领域的内聚性。
内容的提问来源于stack exchange,提问作者Julian Gr
相关产品推荐
相关产品推荐

