跳转到内容

领域服务与应用服务

用途:一段业务规则不知道该放哪、或事务边界与编排逻辑混在一起时查阅

构件 职责 允许包含
领域服务(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 章节说明何时不该建模)