DDD中带副作用的业务逻辑应放在何处?以优惠码适用性校验为例
DDD 优惠码校验逻辑的合理设计方案
核心设计思路
所有和优惠码相关的业务判断逻辑全部内聚在DiscountCode聚合根内部,领域服务仅负责外部依赖调用和流程编排,既避免贫血模型,也不会让逻辑散落各处。
1. 保留原有聚合内无副作用校验
原DiscountCode中的isApplicableToOrder方法不需要改动,继续负责规则1-3的纯内存校验,同时新增规则4的判断逻辑,外部仅需传入查询结果作为参数:
class DiscountCode( val id: DiscountCodeId, val hasAlreadyBeenUsed: Boolean, val startTime: LocalDateTime, val endTime: LocalDateTime ) { // 规则1-3校验:纯函数,无IO副作用 fun isApplicableToOrder(order: Order): Boolean { return startTime.isBefore(now) && endTime.isAfter(now) && order.items.none { it.canNotBeDiscounted } && !hasAlreadyBeenUsed } // 规则4校验逻辑:仍然内聚在优惠码聚合中,不包含查询操作 fun isEligibleForCustomer(customerId: CustomerId, hasValidBinding: Boolean): Boolean { // 后续规则4的扩展逻辑(比如特殊用户白名单、黑名单判断等)都在这里迭代,不用修改上层服务 return hasValidBinding } }
2. 上层领域服务做流程编排
新增仅负责流程调度的领域服务,处理跨聚合的数据库查询,不包含任何业务判断逻辑:
class DiscountValidationService( private val discountCodeRepo: DiscountCodeRepository, private val discountBindingRepo: DiscountCodeCustomerBindingRepository ) { fun canApplyDiscount(codeId: DiscountCodeId, order: Order): Boolean { val discountCode = discountCodeRepo.getById(codeId) ?: return false // 先执行聚合内的规则1-3校验,失败直接返回 if (!discountCode.isApplicableToOrder(order)) return false // 执行跨聚合关联查询,拿到规则4需要的参数 val hasValidBinding = discountBindingRepo.exists(codeId, order.customerId) // 交给聚合完成规则4校验 return discountCode.isEligibleForCustomer(order.customerId, hasValidBinding) } }
设计合理性说明
- 所有优惠码相关的业务规则全部内聚在
DiscountCode类中,后续规则调整只需要修改聚合类,不需要改动上层服务,不会出现逻辑散落的问题 - 聚合类的所有方法都是纯函数,不需要依赖数据库就能做单元测试,可测性极强
- 领域服务仅负责IO操作和流程编排,没有业务逻辑,符合单一职责原则
- 完全避免了大列表嵌入聚合的性能问题,关联查询的开销可控
内容的提问来源于stack exchange,提问作者Grisu47
相关产品推荐
相关产品推荐

