DDD技术咨询:聚合根与领域服务的持久化及服务调用位置选择
DDD核心组件职责边界:聚合根、服务、持久化与锁的最佳实践
先明确核心职责划分原则,再结合你的购物车场景逐一拆解:
- 聚合根:仅负责纯领域逻辑,维护自身状态一致性,不依赖外部IO、持久化或锁机制
- 领域服务:处理跨聚合根的领域逻辑,协调内部领域服务调用
- 应用服务:编排业务流程,调用外部服务适配器、触发持久化、管理锁机制
1. 外部服务调用(地址验证)
结论:在应用服务中调用地址验证适配器,验证通过后再传递合法的地址值对象给购物车聚合根
分析各方案:
- 聚合根直接调用:不可取。聚合根应避免依赖外部IO操作(HTTP调用属于耗时且不可控的IO),会破坏其纯净性与可测试性,偏离领域逻辑核心。
- 值对象调用:不可取。值对象是纯数据载体,仅负责自身格式校验,调用外部服务违背单一职责,且无法处理验证失败后的业务逻辑(如提示用户修改地址)。
- 应用服务作为入口:先调用适配器完成地址验证,验证通过后创建地址值对象,再调用购物车聚合根的
addShippingAddress方法。此时聚合根只需接收合法地址,专注于自身业务逻辑(如检查地址是否重复、是否符合结账状态要求等)。
2. 内部领域服务调用(库存检查)
结论:在领域服务中调用库存服务,确认库存充足后触发购物车聚合根的addProduct操作
分析:
- 聚合根直接调用库存服务:不可取。这会让聚合根依赖其他领域的服务,破坏领域边界,且聚合根无法处理库存不足的业务流程(如提示用户库存不足、推荐替代商品)。
- 领域服务(如
ShoppingCartDomainService)作为协调者:先调用InventoryService检查库存,库存充足时再调用购物车聚合根的addProduct方法,同时可统一处理库存不足的逻辑,保持聚合根仅关注自身状态变更。
3. 持久化操作
结论:在应用服务中触发持久化,聚合根仅提供状态或生成变更事件,由仓储(Repository)执行持久化
分析各方案:
- 聚合根自身处理持久化:违背单一职责。聚合根核心是业务逻辑,持久化属于基础设施层操作,耦合后会导致聚合根难以测试,且无法灵活切换持久化方式。
- 应用服务触发:业务操作完成后(如添加商品成功),调用仓储的
save方法持久化聚合根状态;加载聚合根时,应用服务调用仓储的findById方法获取实例,再传递给业务逻辑使用。这样聚合根保持纯净,仅负责状态管理,仓储负责与数据库交互。
4. 锁与并发控制
结论:在应用服务或基础设施层(仓储)中处理锁,聚合根不参与锁操作
分析各方案:
- 聚合根自身处理锁:不可取。锁属于并发控制的技术细节,与领域逻辑无关,会污染领域模型,增加聚合根非核心职责。
- 应用服务/仓储处理:应用服务在调用聚合根操作前,先获取分布式锁(如Redis锁);或由仓储在加载聚合根时自动启用乐观锁/悲观锁。如果使用Darlean这类Actor框架,Actor自带单线程执行特性,天然避免并发问题,锁逻辑由框架内部处理即可;若采用极简聚合根方案,由应用服务统一管理锁,确保同一购物车的操作串行执行。
结合你方团队经验的调整建议
- Darlean Actor框架场景:Actor可作为聚合根的容器,负责处理锁(单线程执行)、触发持久化,但需将领域逻辑与Actor的技术逻辑分离,避免Actor代码中充斥业务逻辑,保持聚合根的纯净性。
- 极简聚合根方案:可保留该模式,但需明确应用服务的职责边界——应用服务负责编排所有外部调用、持久化、锁操作,聚合根仅做简单的状态变更和格式校验。这种方案适合业务逻辑相对简单的场景,降低团队学习成本,但要避免应用服务过度膨胀成“上帝服务”,尽量拆分领域服务处理跨聚合根的逻辑。
内容的提问来源于stack exchange,提问作者Theo
相关产品推荐
相关产品推荐

