在领域驱动设计(DDD)中,领域服务(Domain Service) 和 聚合方法(即聚合根或其内部实体/值对象上的方法) 是实现业务逻辑的两种核心方式。它们的根本区别在于职责归属、状态持有和协作范围。
下面从多个维度清晰对比两者的区别,并给出实践指导:
一、本质定义
| 概念 | 说明 |
|---|---|
| 聚合方法 | 定义在聚合根(Aggregate Root)或其内部对象上的实例方法,用于封装该聚合自身的业务行为和状态变更。 |
| 领域服务 | 一个无状态的类,用于实现不属于任何单一聚合的业务逻辑,通常涉及多个聚合或无法自然归属的规则。 |
✅ 聚合是有状态的“主角”,领域服务是无状态的“协调者”。
二、核心区别对比表
| 维度 | 聚合方法 | 领域服务 |
|---|---|---|
| 所属对象 | 聚合根(实体) | 独立的服务类 |
| 是否持有状态 | ✅ 是(操作自身状态) | ❌ 否(无实例状态) |
| 业务范围 | 仅限本聚合内部 | 可跨多个聚合 |
| 调用方式 | order.cancel() |
transferService.transfer(a1, a2, amount) |
| DDD 角色 | 核心领域模型的一部分 | 辅助性领域构件 |
| 是否鼓励优先使用 | ✅ 是(富模型原则) | 仅在必要时使用 |
| 依赖外部 | 不应依赖外部服务或仓储 | 可依赖其他聚合、仓储接口等(但不依赖基础设施) |
三、何时使用聚合方法?(优先选择)
当业务逻辑:
- 只操作本聚合的状态
- 是该聚合的自然职责
- 能通过聚合根的方法清晰表达意图
✅ 示例:订单取消
1public class Order {
2 private OrderStatus status;
3
4 public void cancel() {
5 if (status == OrderStatus.SHIPPED) {
6 throw new IllegalStateException("已发货,不能取消");
7 }
8 this.status = OrderStatus.CANCELLED;
9 }
10}
✔️ 完全基于自身状态,是订单的“天职”——放在聚合方法中。
四、何时使用领域服务?(不得已而为之)
当业务逻辑:
- 涉及两个或以上聚合
- 无法合理归属到任一聚合
- 是通用业务规则(如校验、计算、协调)
✅ 示例:转账(涉及两个账户聚合)
1public class TransferService {
2 public void transfer(Account from, Account to, Money amount) {
3 if (!from.canTransfer(amount)) {
4 throw new InsufficientFundsException();
5 }
6 from.debit(amount);
7 to.credit(amount);
8 }
9}
✔️ 操作两个
Account 聚合,无法放在任一账户内部——必须用领域服务。
五、常见错误与反模式
| 错误做法 | 问题 | 正确做法 |
|---|---|---|
把所有逻辑写在 XxxService 中,聚合只有 getter/setter |
贫血模型,违背 DDD | 让聚合拥有行为,成为富模型 |
| 在聚合方法中直接调用 Repository 或发 HTTP 请求 | 混淆关注点,破坏聚合边界 | 聚合只管状态和规则;外部交互由应用层或领域服务(通过接口)协调 |
| 为每个聚合都创建一个 “Service” 类 | 过度设计 | 只有跨聚合或无归属逻辑才需要领域服务 |
六、设计决策流程图(简化版)
1要实现一个业务逻辑?
2 ↓
3是否只操作一个聚合的状态?
4 ↙ 是 ↘ 否
5放在聚合方法中 是否属于某个聚合的自然职责?
6 ↙ 是 ↘ 否
7 放在该聚合中 使用领域服务
七、总结口诀
🔹 能放聚合,绝不外放
🔹 跨聚合作,服务登场
🔹 聚合有状态,服务无状态
🔹 服务不碰基础设施,只靠抽象协作
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于