Pragmatic DDD:让框架成为业务背后的驱动引擎
在动手写第一个聚合根之前,先理解框架为什么这样设计领域层。本文是框架的设计理念宣言,回答"为什么领域层要这样做",不展开具体 API 用法(详见 快速开始 与 推荐项目结构)。
一、核心主张:领域层只声明业务事实
打开一个项目的代码,想找到某条业务规则到底写在哪里,结果翻遍了几个文件都没找到。有时候明明记得“下单后通知仓库”这个逻辑应该在某个地方,可 Controller 里有参数校验,Service 里混着库存扣减和消息发送,Entity 里还藏了一段日志。你想单独拎出那条“通知仓库”的规则,却发现它散落在好几个地方,根本没法一眼看清楚。
pragmatic-ddd 的立场是:通过定义一个清晰的领域层,将其作为系统的指挥中心,完整地承载业务蓝图。在这个指挥中心里,聚合根只负责描述“发生了什么、要做什么、有哪些约束”,至于“何时执行、按什么顺序、失败如何兜底”这类执行职责,则全部交由框架托管。
以下对照表概括了"你写什么"与"框架托管什么":
| 你写的内容(业务意图) | 框架托管的内容(隐形引擎) |
|---|---|
| 聚合根的状态与行为 | 规则校验时机、失败抛异常 |
| Operation 业务操作码 | 回填事件因果、路由持久化策略 |
| 领域事件的收集 | 持久化后发布、清理工作单元、重试兜底 |
| 业务规则(不变量) | 激活条件过滤、failFast 短路 |
| 订阅者的响应蓝图 | 依赖顺序传播、执行条件判定、循环检测 |
| 外部依赖的契约声明 | 架构可视化、防腐适配器注入 |
一句话概括:你描述的业务,就是框架要执行的剧本。
二、三条设计支点
2.1 业务事实与执行时机分离
理念:业务方法只"声明"一次变更——修改属性、记录操作、收集事件——而不决定校验、落库、发布的时机。
框架对应:聚合根业务方法遵循固定的三步式写法;校验与持久化由 CommandExecutor(单聚合)或 UnitOfWork(跨聚合)在固定模板中托管。
public void payment() {
this.status = OrderStatus.PAYED;
this.markModified();
this.recordOperation(OrderOperationRegistry.PAY);
this.collectEvent(() -> OrderPayedEvent.buildEvent(this));
}收益:业务方法纯粹、可预测,校验逻辑不散落各处。 代价:你不能在业务方法里手写校验或发事件——那是框架的职责,越界写会导致逻辑与框架的双重校验机制冲突。
2.2 单一事实来源:因果自动归属
理念:事件的"来源"(operationCode)与"版本"(version)不该由业务代码手动拼装,而应由框架从聚合状态自动推导。
框架对应:recordOperation 后 collectEvent,框架自动把最近一次操作回填为事件 operationCode、把版本号回填为 version;getNewVersion() 幂等,同一工作单元内所有事件共享同一版本号。
收益:因果链成为框架保证的不变量,你不必手动透传,也避免了"谁触发了什么、发生在哪个版本"的拼写错误。 代价:你必须遵守"先记录操作、后收集事件"的次序,否则框架无法推导因果而报错。
2.3 一致性边界:跨聚合走事件而非直改
理念:聚合根是事务一致性边界。边界内的不变量由业务规则保证;边界外的协调不通过"直接改别的聚合",而通过"发布事实 → 订阅者各自响应"。
框架对应:聚合根业务方法内不修改其他聚合;需要外部数据或能力时,在领域层用 @ExternalDependency 声明依赖契约,由基础设施层的防腐适配器(ACL)落地实现。
收益:守住事务一致性边界,跨聚合的协调被显式建模为事件流。 代价:跨聚合一致性从"强一致"变为"最终一致"。建模时需要主动判断某个操作是否跨聚合、能否接受最终一致。
三、遵循理念的收益与代价
框架不是让你少写代码,而是把"驱动"从你的领域层心智负担里移除。遵循这条边界,你得到与放弃的是一组明确权衡:
| 维度 | 你获得 | 你放弃 |
|---|---|---|
| 可读性 | 领域层宏观可读,业务人员可通读业务流转 | 微观层面的局部内聚(为换取宏观可读) |
| 技术无关性 | 换数据库 / 消息中间件 / 计算实现方式,领域层不改 | 在领域层自由写 SQL、MQ、远程调用的能力 |
| 一致性 | 事务边界清晰、可预期 | 跨聚合的强一致(改为事件最终一致) |
取舍即理念:领域层的纯度是有价格的。这份价格换来的,是领域层成为一份不受技术干扰的"作战计划"。
四、理念落地为框架约束
每条约束都源于上面的理念。理解"因为理念是 X,所以必须 Y",比死记更可靠:
| 理念 | 对应的框架约束 | 违背的后果 |
|---|---|---|
| 只声明事实 | 业务方法三步式,不内联校验/发布 | 校验逻辑与框架双重校验机制冲突 |
| 因果自动归属 | 先 recordOperation 后 collectEvent | 抛 OperationException |
| 构造期事实未定 | ID 后生成时用延迟事件 collectEvent(Supplier) | 事件定格到错误的 entityId |
| 声明与实现分离 | 领域层 dependency/ 只声明接口,ACL 实现在基础设施层 | 领域层被技术实现污染 |
| 事件会累积 | 应用层发布后调用 clearWorkUnitState() | 同一事件被重复发布 |
| 反射扫描注册 | 注册表子类必须为 public | 包级私有导致消息码 / 操作码未注册 |
⚠️ 重要约束:
OperationRegistry/BrokenRuleRegistry子类必须是public。框架在io.pragmatic.ddd.base包内通过反射field.get(null)扫描子类的static字段自动注册;若子类为包级私有,IllegalAccessException被静默吞掉,导致消息码未注册(getRuleDescription返回空串)或操作码未注册(TriggeredOperations.put抛OperationException)。
五、总结速查
| 设计理念 | 对应的框架约束 | 关键落点 |
|---|---|---|
| 领域层只声明事实 | 业务方法三步式,执行职责交框架 | 领域层是"指挥中心"而非执行者 |
| 事实与执行分离 | 校验 / 持久化 / 发布由 CommandExecutor / UnitOfWork 托管 | 业务方法保持纯粹 |
| 因果自动归属 | 先 recordOperation 后 collectEvent | 事件携带 operationCode 与 version |
| 一致性边界 | 跨聚合靠事件,聚合内不碰其他聚合 | 边界内规则保证,边界外最终一致 |
| 声明与实现分离 | @ExternalDependency 声明 + ACL 实现 | 领域层零技术污染 |
下一步:快速开始 →