领域服务与应用服务
用途:一段业务规则不知道该放哪、或事务边界与编排逻辑混在一起时查阅
定义
「定义」小节链接| 构件 | 职责 | 允许包含 |
|---|---|---|
| 领域服务(Domain Service) | 表达不属于任何单一实体的业务操作 | 业务规则、跨对象计算、领域策略 |
| 应用服务(Application Service) | 面向用例的编排:取聚合、调方法、发事件、提交事务 | 事务边界、权限校验、幂等控制、装配返回 |
判据一句话:如果这段逻辑删掉后,业务规则就没人守了,它是领域逻辑;如果只是“按什么顺序调用”,它是应用逻辑。
由来与要解决的问题
「由来与要解决的问题」小节链接不区分这两类职责时,代码会自然长成一个 XxxService:里面既有参数校验、事务、远程调用,又有状态判断与业务规则。后果是规则无法单测(要起数据库和远程服务)、无法复用(换个入口就得复制)、变更影响面失控。
应用服务的标准骨架
「应用服务的标准骨架」小节链接用例:支付订单1. 按 ID 加载聚合根(仓储)2. 权限与前置校验(是否可操作该资源)3. 调用聚合方法:order.pay(paymentId) ← 规则在聚合内4. 保存聚合(仓储)5. 提交事务后发布领域事件(Outbox 落库同事务)6. 组装返回结果第 3 步之外不应出现业务判断。如果应用服务里出现 if (order.status == X && order.amount > Y),说明规则正在外流。
事务边界通常在应用服务:一个用例一个事务,对应一个聚合的修改。需要改多个聚合时,不要扩事务,改为多事务 + 事件协调(见聚合与最终一致)。
领域服务的正当用途
「领域服务的正当用途」小节链接| 场景 | 例子 |
|---|---|
| 计算需要多个输入,不天然属于任一对象 | 利息计算、运费估算、折扣叠加策略 |
| 依赖外部能力的业务判定 | 需要调用风控结果才能决定的业务规则(接口在领域层,实现在基础设施层) |
| 跨聚合的纯判定 | “该客户是否满足升级条件”(只读判断,不修改状态) |
| 不适合放进实体的操作 | 生成唯一编号、合并两个对象 |
与其他概念的关系
「与其他概念的关系」小节链接- 聚合根:规则的归属地;服务只在规则无法归属时出现。
- 资源库:应用服务通过仓储取聚合,领域服务通常不直接访问仓储(需要数据时由调用方传入)。
- 接口层/Controller:只做协议转换与参数绑定,不含编排逻辑,否则同一用例无法被定时任务、消息消费者复用。
- 规范模式:可组合的业务判定条件适合抽成 Specification,而不是散在服务里。
常见误区
「常见误区」小节链接| 误区 | 表现 | 修正 |
|---|---|---|
| 领域服务成新的垃圾场 | 说不清归属的逻辑全塞进 XxxDomainService |
追问“这条规则改变了哪个对象的状态”,多数能落回聚合 |
| 应用服务写规则 | 状态判断、金额校验在应用层 | 上收聚合;应用层只判断“是否允许发起” |
| Controller 承载编排 | 定时任务与 MQ 消费者复制一套逻辑 | 用例入口收敛到应用服务,Controller 变薄 |
| 一个 Service 包揽一切 | 类超千行、依赖十几个组件 | 按用例拆分应用服务,按能力拆领域服务 |
| 领域服务依赖基础设施细节 | 直接注入 Mapper、HTTP 客户端 | 领域层定义接口,基础设施层实现(依赖倒置) |
开源参考
「开源参考」小节链接Spring 的 @Transactional 边界实践、COLA 的 AppService 分层模板、Axon Framework 的 Command Handler 与应用服务划分,都提供了可对照的骨架。
延伸阅读
「延伸阅读」小节链接- Eric Evans《领域驱动设计》第 5、7 章(服务与层)
- Vaughn Vernon《实现领域驱动设计》第 3、14 章(应用服务与事务脚本对照)
- Martin Fowler《企业应用架构模式》(Transaction Script 章节说明何时不该建模)