DDK 四层架构
适配层、应用层、领域层、基础设施层。领域层承载业务规则且不依赖任何框架,基础设施层通过实现领域层定义的端口反向依赖它。用
ddk-layer4-archetype生成,参考实现见ddk-examples/ddk-example-user。
各层职责
| 层 | 职责 | 不做什么 |
|---|---|---|
| adapter | 协议适配:校验 *Request、转成命令、包装 ApiResponse | 不写业务判断,不碰领域对象 |
| application | 用例编排与事务:取出聚合、调用领域方法、保存、*Response.from(...) | 不写业务规则 |
| domain | 业务规则:聚合根守卫不变量、值对象自我校验、登记领域事件、声明端口 | 不依赖 Spring / ORM / Jackson |
| infrastructure | 技术实现:仓储、Entity ↔ PO 转换、Mapper、外部调用 | 不把 PO 与框架注解泄漏回领域层 |
生成项目
bash
mvn archetype:generate \
-DarchetypeGroupId=com.domaindrivenkit -DarchetypeArtifactId=ddk-layer4-archetype -DarchetypeVersion=0.10.0 \
-DgroupId=com.acme -DartifactId=order-service -Dpackage=com.acme.order -DinteractiveMode=false包结构
text
com.acme.order
├── Application
├── adapter
│ └── controller REST 控制器与 *Request
├── application
│ ├── command 写操作入参 *Command(record)
│ ├── query 读操作条件 *Query,分页继承 PageQuery
│ ├── response 对外响应 *Response,static from(...) 转换
│ ├── service 应用服务,编排与事务
│ └── handler 领域事件订阅方,按用途选择时机,见下文
├── domain 不依赖任何框架
│ ├── model 聚合根、类型化标识、值对象(record)、枚举
│ ├── event 领域事件,过去式命名
│ ├── acl 仓储契约 *Repository、外部能力端口 *Gateway
│ ├── service 跨聚合的领域服务
│ └── error 领域错误码
└── infrastructure
├── acl/impl 端口实现,仓储继承 GenericRepositoryImpl
├── converter Entity ↔ PO 显式转换器
└── orm
├── po 持久化对象 *PO,@Version 对应聚合版本号
└── mapper MyBatis-Plus Mapper生成的项目里每个包都有一份 package-info.java,内容与上表一致。
依赖倒置
领域层声明「需要能存取用户」(UserRepository)、「需要能生成标识」(UserIdGenerator),基础设施层给出实现。依赖方向永远指向领域层,领域模型因此可以脱离容器做纯单元测试。
生成的项目自带 ArchitectureTest,通过 ArchGuard.check 在构建期一次检查四条规则,违规明细与修复建议写入 target/archguard/:
| 规则 | 约束 |
|---|---|
LAYERED_ARCHITECTURE_RULE | 四层依赖方向;基础设施层不被任何层引用,应用服务也只依赖领域端口 |
DOMAIN_MUST_NOT_DEPEND_ON_FRAMEWORKS | 领域层不依赖框架 |
DOMAIN_MUST_NOT_DEPEND_ON_OUTER_LAYERS | 领域层不依赖外层 |
DDK_INTERNALS_MUST_NOT_BE_USED | 不引用 DDK 的 internal 包 |
详见架构守卫。必须点名实现类的装配放到层外(例如 config 包)的 @Configuration 类里,分层规则豁免它。
领域事件的订阅时机
application/handler 里的订阅方按用途选择执行时机(生成项目的 ddk-add-domain-event 技能里有同样的表):
| 目的 | 时机 | 保证 |
|---|---|---|
把领域事件翻译成对外的集成消息(@IntegrationEvent) | @EventListener,在原事务里同步执行 | 消息和业务数据一起提交、一起回滚;提交后由事件 starter 投递 |
标记读模型需要刷新(projections.markDirty) | @EventListener,在原事务里同步执行 | 不会出现「数据改了、刷新标记没写」 |
| 可以丢的通知,例如刷新进程内的缓存、打日志 | @TransactionalEventListener(phase = AFTER_COMMIT) | 只在提交后执行;进程这时崩溃就丢了 |
| 必须完成、又不该拖慢原事务的工作,例如发通知邮件 | 发一条集成事件,由消费方处理 | 持久化、可重投,重启不丢 |
0.8 的模板对所有订阅都建议 AFTER_COMMIT,用它做「必须完成」的事,提交之后、执行之前进程崩溃,这件事就悄悄没做了。
相关阅读
- 分层、应用服务与仓储:四层的职责划分与严格分层的取舍