Skip to content

Pragmatic DDD:让框架成为业务背后的驱动引擎

在动手写第一个聚合根之前,先理解框架为什么这样设计领域层。本文是框架的设计理念宣言,回答"为什么领域层要这样做",不展开具体 API 用法(详见 快速开始推荐项目结构)。

一、核心主张:领域层只声明业务事实

打开一个项目的代码,想找到某条业务规则到底写在哪里,结果翻遍了几个文件都没找到。有时候明明记得“下单后通知仓库”这个逻辑应该在某个地方,可 Controller 里有参数校验,Service 里混着库存扣减和消息发送,Entity 里还藏了一段日志。你想单独拎出那条“通知仓库”的规则,却发现它散落在好几个地方,根本没法一眼看清楚。

pragmatic-ddd 的立场是:通过定义一个清晰的领域层,将其作为系统的指挥中心,完整地承载业务蓝图。在这个指挥中心里,聚合根只负责描述“发生了什么、要做什么、有哪些约束”,至于“何时执行、按什么顺序、失败如何兜底”这类执行职责,则全部交由框架托管。

以下对照表概括了"你写什么"与"框架托管什么":

你写的内容(业务意图)框架托管的内容(隐形引擎)
聚合根的状态与行为规则校验时机、失败抛异常
Operation 业务操作码回填事件因果、路由持久化策略
领域事件的收集持久化后发布、清理工作单元、重试兜底
业务规则(不变量)激活条件过滤、failFast 短路
订阅者的响应蓝图依赖顺序传播、执行条件判定、循环检测
外部依赖的契约声明架构可视化、防腐适配器注入

一句话概括:你描述的业务,就是框架要执行的剧本。


二、三条设计支点

2.1 业务事实与执行时机分离

理念:业务方法只"声明"一次变更——修改属性、记录操作、收集事件——而不决定校验、落库、发布的时机。

框架对应:聚合根业务方法遵循固定的三步式写法;校验与持久化由 CommandExecutor(单聚合)或 UnitOfWork(跨聚合)在固定模板中托管。

java
public void payment() {
    this.status = OrderStatus.PAYED;
    this.markModified();
    this.recordOperation(OrderOperationRegistry.PAY);
    this.collectEvent(() -> OrderPayedEvent.buildEvent(this));
}

收益:业务方法纯粹、可预测,校验逻辑不散落各处。 代价:你不能在业务方法里手写校验或发事件——那是框架的职责,越界写会导致逻辑与框架的双重校验机制冲突。

2.2 单一事实来源:因果自动归属

理念:事件的"来源"(operationCode)与"版本"(version)不该由业务代码手动拼装,而应由框架从聚合状态自动推导。

框架对应recordOperationcollectEvent,框架自动把最近一次操作回填为事件 operationCode、把版本号回填为 versiongetNewVersion() 幂等,同一工作单元内所有事件共享同一版本号。

收益:因果链成为框架保证的不变量,你不必手动透传,也避免了"谁触发了什么、发生在哪个版本"的拼写错误。 代价:你必须遵守"先记录操作、后收集事件"的次序,否则框架无法推导因果而报错。

2.3 一致性边界:跨聚合走事件而非直改

理念:聚合根是事务一致性边界。边界内的不变量由业务规则保证;边界外的协调不通过"直接改别的聚合",而通过"发布事实 → 订阅者各自响应"。

框架对应:聚合根业务方法内不修改其他聚合;需要外部数据或能力时,在领域层用 @ExternalDependency 声明依赖契约,由基础设施层的防腐适配器(ACL)落地实现。

收益:守住事务一致性边界,跨聚合的协调被显式建模为事件流。 代价:跨聚合一致性从"强一致"变为"最终一致"。建模时需要主动判断某个操作是否跨聚合、能否接受最终一致。


三、遵循理念的收益与代价

框架不是让你少写代码,而是把"驱动"从你的领域层心智负担里移除。遵循这条边界,你得到与放弃的是一组明确权衡:

维度你获得你放弃
可读性领域层宏观可读,业务人员可通读业务流转微观层面的局部内聚(为换取宏观可读)
技术无关性换数据库 / 消息中间件 / 计算实现方式,领域层不改在领域层自由写 SQL、MQ、远程调用的能力
一致性事务边界清晰、可预期跨聚合的强一致(改为事件最终一致)

取舍即理念:领域层的纯度是有价格的。这份价格换来的,是领域层成为一份不受技术干扰的"作战计划"。


四、理念落地为框架约束

每条约束都源于上面的理念。理解"因为理念是 X,所以必须 Y",比死记更可靠:

理念对应的框架约束违背的后果
只声明事实业务方法三步式,不内联校验/发布校验逻辑与框架双重校验机制冲突
因果自动归属recordOperationcollectEventOperationException
构造期事实未定ID 后生成时用延迟事件 collectEvent(Supplier)事件定格到错误的 entityId
声明与实现分离领域层 dependency/ 只声明接口,ACL 实现在基础设施层领域层被技术实现污染
事件会累积应用层发布后调用 clearWorkUnitState()同一事件被重复发布
反射扫描注册注册表子类必须为 public包级私有导致消息码 / 操作码未注册

⚠️ 重要约束OperationRegistry / BrokenRuleRegistry 子类必须是 public。框架在 io.pragmatic.ddd.base 包内通过反射 field.get(null) 扫描子类的 static 字段自动注册;若子类为包级私有,IllegalAccessException 被静默吞掉,导致消息码未注册(getRuleDescription 返回空串)或操作码未注册(TriggeredOperations.putOperationException)。


五、总结速查

设计理念对应的框架约束关键落点
领域层只声明事实业务方法三步式,执行职责交框架领域层是"指挥中心"而非执行者
事实与执行分离校验 / 持久化 / 发布由 CommandExecutor / UnitOfWork 托管业务方法保持纯粹
因果自动归属recordOperationcollectEvent事件携带 operationCodeversion
一致性边界跨聚合靠事件,聚合内不碰其他聚合边界内规则保证,边界外最终一致
声明与实现分离@ExternalDependency 声明 + ACL 实现领域层零技术污染

下一步:快速开始 →