DDD中Explicit State Modeling落地实践相关疑问求解
背景
我最近在研究聚合根的状态与生命周期管理,发现和状态模式相比,采用显式状态建模(Explicit State Modeling/Explicit Modeling)的方案更简洁,能够让领域内的显式概念自行处理对应行为,完全符合我的预期。我参考了Scott Millett与Nick Tune所著《领域驱动设计的模式、原则与实践》第16章衍生的相关内容,以及对应的代码样例。
不过现有资料对这套方案的介绍普遍比较简略,我作为DDD入门者在落地过程中出现了概念混淆,整理了当前的几个核心困惑:
- 按照参考示例,若要查询同时包含开门、关门状态的全部门列表,返回的结果集应映射到哪个领域实体?
- 如果所有显式状态模型都是实体/聚合,对应的聚合根是什么?
- 聚合根和这些显式建模的实体之间不存在引用关系是否符合规范?
- 若聚合根(比如通用的Door实体)返回显式状态实体,仓储如何在不暴露实体/聚合状态的前提下完成持久化?
- 这些显式状态实体是否本身就是各自聚合的根?
我暂未梳理清楚完整逻辑,无法提供自有代码,希望下文的参考片段可以作为判断依据。如果有结合显式建模实现其他DDD组件的示例项目推荐也非常有帮助,我已经查阅了大量相关仓库,但大多不符合需求。
注:我当前未使用CQRS
参考代码示例
Medium文章参考示例
interface ClosableDoor { public function close(); } // 显式状态实现(第三次优化版本) class CloseDoorService() { // 注入依赖 public function execute($doorId) { $door = $this->doorRepository->findClosableOfId($doorId); if (!$door) { throw new ClosableDoorNotFound(); } $door = $door->close(); $this->doorRepository->persist($door); } }
书籍参考示例
// 以下实体集合替代了原本使用状态模式的OnlineTakeawayOrder实体 public class InKitchenOnlineTakeawayOrder { public InKitchenOnlineTakeawayOrder(Guid id, Address address) { // 初始化逻辑省略 this.Id = id; this.Address = address; } public Guid Id { get; private set; } public Address Address { get; private set; } // 仅包含自身实际支持的方法 // 返回新的状态实例,强制调用方感知状态变化 public InOvenOnlineTakeawayOrder Cook() { // 业务逻辑省略 return new InOvenOnlineTakeawayOrder(this.Id, this.Address); } } public class InOvenOnlineTakeawayOrder { public InOvenOnlineTakeawayOrder(Guid id, Address address) { // 初始化逻辑省略 this.Id = id; this.Address = address; } public Guid Id { get; private set; } public Address Address { get; private set; } public CookedOnlineTakeawayOrder TakeOutOfOven() { // 业务逻辑省略 return new CookedOnlineTakeawayOrder(this.Id, this.Address); } }
疑问解答
多状态列表查询的实体映射问题
不需要把查询结果强制映射到写路径的领域实体,显式状态建模的核心是约束写操作的领域规则,读场景下如果需要聚合多状态的统一视图,可以直接定义专门的读模型DTO承载返回结果,和是否使用CQRS没有冲突。如果一定要在领域层复用统一抽象,可以定义包含所有状态共有属性的Door接口/抽象类,开放、关闭状态的实体都实现该接口,查询时返回接口类型的集合即可。显式状态模型的聚合根归属
所有同生命周期、同业务ID的显式状态实体,本质是同一个聚合根在不同阶段的形态,聚合根的ID始终唯一(比如订单ID、门ID),不需要为每个状态单独定义聚合根,可以把这些状态类理解为聚合根的「类型化状态实例」,核心身份标识始终统一。聚合根与显式状态实体无引用的合规性
完全符合规范。显式状态建模的核心就是用类型系统替代隐式的状态字段,原本聚合根里的state枚举被替换成了不同的类型实现,二者不需要额外的引用关系,每个状态实体本身就是特定阶段的聚合根实例。仓储持久化的实现方案
可以让仓储依赖统一的抽象接口,不需要感知具体的状态类型,持久化时只需要提取所有状态实例共有的身份标识、属性、状态枚举值存入数据库即可,状态到枚举值的映射可以放在状态类内部实现(比如每个状态类暴露一个只读的StateCode属性),不会泄露实体的内部业务逻辑。查询时,仓储可以根据数据库存储的状态字段,实例化对应类型的状态实体返回即可,这部分逻辑完全封装在仓储内部,领域层不需要感知。显式状态实体是否是独立聚合根
不是。所有共享同一个业务ID的状态实例,都是同一个业务聚合的不同表现形态,聚合根的业务边界始终是同一个业务对象(比如同一个门、同一个订单),不会因为状态变化改变聚合边界。只有当某个状态下的实体产生了完全独立的生命周期、业务规则、身份标识时,才需要拆分为独立的聚合根。
内容的提问来源于stack exchange,提问作者Mazen Elkashef

