领域服务
本文档属于 pragmatic-ddd 使用文档
core系列,定义领域服务(io.pragmatic.ddd.service.IDomainService)的分类、契约与分层约束。 阅读前建议先完成 领域建模。本系列相关文档:业务规则引擎 · 领域事件 · 应用服务。
1. 概述
1.1 核心定位
领域服务是领域层声明、应用层实现的契约接口:领域层通过 extends IDomainService 定义"需要什么能力",应用层提供具体实现。该契约用于承载无法归入单一聚合根的逻辑——事件订阅、跨聚合校验、属性计算与领域原语供给。框架不提供领域服务运行时容器,仅通过标记接口 + 注解承载可反射读取的分类元信息,实现在应用层注册与装配。
1.2 概念层级与依赖关系
类型继承关系:
IDomainService (io.pragmatic.ddd.service) 标记接口,提供 category()
├── IEventSubscriberService<T> (service) extends IDomainService, IHandle<T>
├── ICheckRuleService<T> (service) extends IDomainService, ICheckRule<T>
├── IAttributeCalculatorService (service) extends IDomainService (第三类属性计算标记子接口)
├── ICapabilityProviderService (service) extends IDomainService
└── IEntityPropertyCalculator<T,E,R> (base) extends IDomainService (第三类绑定实体属性的泛型契约)
IHandle<T> (io.pragmatic.ddd.event.spi) 事件处理端口,声明 void handleEvent(T)模块/分层依赖约束:
| 层 | 职责 | 编译期依赖边界 |
|---|---|---|
| 领域层 | 定义接口(契约),声明"做什么" | 仅依赖 io.pragmatic.ddd.service / io.pragmatic.ddd.base 与领域类型;不依赖基础设施(数据库、HTTP 客户端、消息中间件) |
| 应用层 | implements 提供实现 | 可依赖基础设施,并将实现注册/装配到框架运行时(Spring 容器或事件总线) |
1.3 与经典 DDD 的认知差异
经典 DDD 将领域服务定义为"当某业务逻辑不属于任何一个实体或值对象时,就把它放入领域服务"的兜底概念,典型场景为跨聚合操作(如银行转账)、实体自身无法处理的复杂计算、需多聚合协作完成的流程。该定义的核心问题是"不属于实体"依赖主观判断,缺乏明确边界:同一段逻辑,不同开发者可能归入聚合根,也可能归入领域服务,归属结论不一致。
本框架的领域服务与经典 DDD 理论中的领域服务在定位上有本质区别。经典 DDD 将领域服务定义为"当逻辑不属于任何实体或值对象时的兜底容器",本框架将其重定义为领域层声明、应用层实现的契约(端口)。两者差异如下:
| 维度 | 经典 DDD 领域服务 | 本框架领域服务 |
|---|---|---|
| 定义形态 | 直接编写实现类,承载业务逻辑 | 领域层定义接口(extends IDomainService),应用层 implements 提供实现 |
| 分类 | 单一笼统概念,无内置分类 | 四类明确分类(事件订阅 / 校验规则 / 属性计算 / 能力供给),由方法签名判定 |
| 跨聚合协作 | 鼓励在领域服务中同步编排多个聚合 | 跨聚合协作通过领域事件驱动,由多个订阅者分别响应,不在单个服务内同步编排 |
| 依赖方向 | 实现可直接依赖数据库、外部 API 等基础设施 | 领域层接口不依赖基础设施;仅应用层实现可依赖基础设施 |
| 归类判定 | 由开发者主观判断"是否属于实体" | 由契约的方法签名与语义客观判定(见 §2.5 判定流程) |
经典 DDD 常用"银行转账"作为领域服务范例。该范例在本框架中不适用:真实转账多为跨系统调用(属应用/基础设施层职责),即便同系统内部转账也更宜通过事件驱动异步处理,而非在一个领域服务中同步编排两个账户聚合。定义本框架的领域服务时,不要套用"银行转账式"跨聚合编排范式。
2. 四类领域服务的严格定义
任一 extends IDomainService 的契约,按其"方法形态 + 业务语义"唯一落入以下一类(互斥且完备)。
2.1 第一类:事件订阅领域服务(EVENT_SUBSCRIBER)
领域层声明"在某领域事件发生后,执行某业务动作"的契约。形态标志:继承 IHandle<T>,由此声明所关注事件类型 T 与处理方法 handleEvent(T)。
| 项 | 说明 |
|---|---|
| 基类接口 | IEventSubscriberService<T extends IDomainEvent> extends IDomainService, IHandle<T> |
| 方法 | void handleEvent(T event)(来自 IHandle<T>,领域层不实现) |
| 触发 | 事件总线在 T 发布后路由调用;框架不扫描 IHandle 实现,Spring 与非 Spring 环境都须经 IEventRegistry.registerSubscriber 显式注册 |
| 语义边界 | 仅响应已发生事件做后续动作;不主动编排跨聚合写操作链路 |
2.2 第二类:业务规则领域服务(BUSINESS_RULE)
领域层声明"对某业务对象执行一条可复用校验,给出通过/拒绝结论"的契约。形态标志:方法返回 RuleCheckResult。
| 项 | 说明 |
|---|---|
| 基类接口 | ICheckRuleService<T> extends IDomainService, ICheckRule<T> |
| 方法 | 自定义校验方法,返回 RuleCheckResult(通常命名为 check(...)) |
| 参数 | 领域对象、值对象或领域参数(不得是应用层 Command / HttpRequest) |
| 返回值 | RuleCheckResult.pass() / RuleCheckResult.fail(Object[]) |
| 语义边界 | 只判断、不改状态、不写库 |
与聚合根内部 IRule<T> / ICheckRule<T> 细粒度不变量校验互补:跨聚合、入参前置、复杂组合业务规则用本类。详见 聚合业务规则(OrderRule 范式)。
2.3 第三类:属性计算(类型转换)领域服务(ATTRIBUTE_CALCULATOR)
领域层声明"基于一个或多个领域输入,推导出某领域属性值 / 值对象"的契约。形态标志:存在"由输入推导输出"的计算方法,输入输出均为领域类型。
| 项 | 说明 |
|---|---|
| 基类接口 | IAttributeCalculatorService extends IDomainService(标记子接口);绑定实体属性时复用 IEntityPropertyCalculator<T,E,R> extends IDomainService |
| 方法 | R calculate(...)(自由计算)或 R calculate(T source, E entity)(实体属性计算) |
| 参数 | 领域对象 / 值对象 / 上下文(IEntityPropertyCalculator 中 entity 在创建场景为 null) |
| 语义边界 | 输出是由输入"推导"出的属性值或视图对象,通常可替换为纯领域计算 |
2.4 第四类:领域工厂 / 能力供给领域服务(CAPABILITY_PROVIDER)
领域层声明"需由应用层落地、且通常依赖基础设施才能提供的领域能力(典型为生成某领域原语/对象)"的契约。形态标志:方法无(或仅简单领域参数)输入,却新生产出领域原语/对象。
| 项 | 说明 |
|---|---|
| 基类接口 | ICapabilityProviderService extends IDomainService |
| 方法 | 产出方法,通常 T generate() / T nextId() / T createXxx(...) |
| 返回值 | 领域原语(如 long ID)或领域对象 |
| 语义边界 | 声明"我需要能产生 X 的能力",具体算法(雪花 / 数据库序列 / UUID)由应用层决定;通常无法用纯领域逻辑替代 |
2.5 四类的互斥与判定流程
任一 extends IDomainService 的契约,按下表唯一归类(自上而下,命中即止):
| 判定序 | 检查项 | 命中条件 | 归类 | 枚举值 |
|---|---|---|---|---|
| 1 | 是否 extends IHandle<T> | 继承 IHandle,响应领域事件 | 事件订阅 | EVENT_SUBSCRIBER |
| 2 | 方法是否返回 RuleCheckResult | 校验方法返回 RuleCheckResult | 业务规则 | BUSINESS_RULE |
| 3 | 方法是否"由领域输入推导领域输出" | 有 calculate(...),输入输出均为领域类型 | 属性计算 | ATTRIBUTE_CALCULATOR |
| 4 | 方法是否"无/少输入却新生产领域原语/对象" | 有 generate() / nextId() 等产出方法 | 能力供给 | CAPABILITY_PROVIDER |
四类互斥:一个契约只会命中其中一项。若同时命中多项,说明接口抽象错误,应拆分。未标注
@DomainService时category()返回UNKNOWN,不在上述四类之内。
3. 分类标记:@DomainService 与 category()
IDomainService 提供 category() 默认方法,配合 @DomainService 注解承载可反射读取的业务元信息(对称于依赖体系的 @ExternalDependency)。
3.1 顶层接口契约
package io.pragmatic.ddd.service;
public interface IDomainService {
/** 返回领域服务分类,默认读取注解,未标注则返回 DomainServiceCategory.UNKNOWN。 */
default DomainServiceCategory category() {
DomainService annotation = findAnnotation(this.getClass());
return annotation == null ? DomainServiceCategory.UNKNOWN : annotation.category();
}
}category() 的注解查找规则(findAnnotation):
- 先查实现类上的
@DomainService; - 未命中则沿所实现接口向上递归查找;
- 仍未命中则沿父类向上递归查找;
- 到达
Object仍无则返回null,category()返回UNKNOWN。
3.2 注解字段说明
package io.pragmatic.ddd.service;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DomainService {
DomainServiceCategory category(); // 服务分类,必填,须与 IDomainService.category() 反映的分类一致
String description() default ""; // 业务描述:这个领域服务是干什么的
String targetName() default ""; // 关联对象名(按 category 语义解释,见下表)
}category | targetName 语义 |
|---|---|
EVENT_SUBSCRIBER | 处理的事件名(如 OrderPaidEvent) |
BUSINESS_RULE | 作用的领域对象(如 Order) |
ATTRIBUTE_CALCULATOR | 作用的领域对象(如 Order/OrderItem) |
CAPABILITY_PROVIDER | 产出的领域原语/对象(如 OrderId) |
3.3 分类枚举
package io.pragmatic.ddd.service;
public enum DomainServiceCategory {
EVENT_SUBSCRIBER, // 事件订阅
BUSINESS_RULE, // 业务规则
ATTRIBUTE_CALCULATOR, // 属性计算(类型转换)
CAPABILITY_PROVIDER, // 领域工厂 / 能力供给
UNKNOWN // 未分类(向后兼容)
}3.4 基类接口清单与注解声明
四类契约继承对应基类接口,并在接口上标注 @DomainService 作为主声明;category() 统一由注解读取。
// 第一类:事件订阅(service 包)
public interface IEventSubscriberService<T extends IDomainEvent>
extends IDomainService, IHandle<T> { }
// 第二类:业务规则(service 包)
public interface ICheckRuleService<T> extends IDomainService, ICheckRule<T> { }
// 第三类:属性计算(service 包,标记子接口)
public interface IAttributeCalculatorService extends IDomainService { }
// 第四类:能力供给(service 包)
public interface ICapabilityProviderService extends IDomainService { }
// 第三类扩展:绑定实体属性的泛型契约(base 包)
public interface IEntityPropertyCalculator<T, E, R> extends IDomainService {
R calculate(T source, E entity); // entity 在创建场景为 null
}注解用法示例
@DomainService(category = DomainServiceCategory.EVENT_SUBSCRIBER,
targetName = "OrderPaidEvent",
description = "订单支付成功后按实付金额发放积分")
public interface IOrderPaidPointsGrantHandle
extends IDomainService, IHandle<OrderPaidEvent> { }4. 定义方式(领域层)
四类契约均只在领域层声明接口,不含实现。接口名必须体现业务意图,不得使用泛化占位词 Handler / Processor / Rule 作为接口名。
4.1 事件订阅契约
@DomainService(category = DomainServiceCategory.EVENT_SUBSCRIBER,
targetName = "OrderPaidEvent",
description = "订单支付成功后向用户发送短信通知")
public interface IOrderPaidSmsNotifyHandle
extends IDomainService, IHandle<OrderPaidEvent> {
// 继承 IHandle<T> 即声明 handleEvent(OrderPaidEvent),无需重复声明
}继承 IEventSubscriberService<T> 与上式的双继承写法等价;同一项目内选定一种并保持统一。一个契约只订阅一类事件,同一事件可有多个平级订阅者(发短信 / 发积分 / 物化 ES 副本 / 物化 Redis 副本)。
4.2 校验规则契约
@FunctionalInterface
public interface IOrderAmountLimitRule extends IDomainService {
RuleCheckResult check(BigDecimal amount);
}4.3 属性计算契约
自由计算场景,继承第三类标记子接口:
public interface IOrderTotalCalculator extends IAttributeCalculatorService {
OrderTotal calculate(List<OrderItem> items);
}绑定具体实体属性的场景,复用 base 包泛型基契约:
public interface IOrderTotalPriceCalculator
extends IEntityPropertyCalculator<TotalPriceContext, Order, BigDecimal> {
}4.4 能力供给契约
public interface IOrderIdGenerator extends IDomainService {
long generate();
}5. 实现方式(应用层)
应用层 implements 契约提供实现,可访问基础设施资源。实现类与领域层契约语义镜像对应。
@Component
public class OrderPaidSmsNotifyHandle implements IOrderPaidSmsNotifyHandle {
private final OrderRepository orderRepository;
private final IUserDependency userDependency;
private final ISmsDependency smsDependency;
public OrderPaidSmsNotifyHandle(
OrderRepository orderRepository,
IUserDependency userDependency,
ISmsDependency smsDependency) {
this.orderRepository = orderRepository;
this.userDependency = userDependency;
this.smsDependency = smsDependency;
}
@Override
public void handleEvent(OrderPaidEvent event) {
Long id = Long.valueOf(event.getEntityId());
Order order = orderRepository.findById(id);
if (order == null) {
return;
}
Customer customer = order.getCustomer();
String mobile = userDependency.getUserMobile(customer.getCustomerId().toString());
if (mobile == null || mobile.isBlank()) {
return;
}
String content = "您的订单 " + order.getEntityId()
+ " 已支付成功,实付金额 " + order.getActualAmount().getAmount() + " 元";
smsDependency.sendSms(new SmsMessage(mobile, content));
}
}事件只携带聚合标识,因此 handleEvent 内先 findById 回源聚合再取权威状态;对外部系统的调用经领域依赖端口(IDependency 子接口),其防腐适配器由基础设施层提供。
@Component
public class OrderAmountLimitRule implements IOrderAmountLimitRule {
private static final BigDecimal MAX_AMOUNT = new BigDecimal("50000");
@Override
public RuleCheckResult check(BigDecimal amount) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
return RuleCheckResult.fail(new Object[]{amount});
}
if (amount.compareTo(MAX_AMOUNT) > 0) {
return RuleCheckResult.fail(new Object[]{amount, MAX_AMOUNT});
}
return RuleCheckResult.pass();
}
}事件订阅实现必须显式注册到事件总线(框架不扫描 IHandle 实现,Spring 环境同样如此)。注册集中在应用层的一张注册表里:
@Configuration
public class OrderEventSubscriberRegistry {
public OrderEventSubscriberRegistry(IEventRegistry evtManager,
OrderDataSyncEsProjectionHandle orderDataSyncEsProjectionHandle,
OrderRedisCacheHandle orderRedisCacheHandle,
OrderPaidSmsNotifyHandle orderPaidSmsNotifyHandle,
OrderPaidPointsGrantHandle orderPaidPointsGrantHandle) {
evtManager.registerSubscriber("es", OrderDataSyncEvent.class, orderDataSyncEsProjectionHandle);
evtManager.registerSubscriber("redis-cache", OrderDataSyncEvent.class, orderRedisCacheHandle);
evtManager.registerSubscriber("sms-notify-on-order-paid", OrderPaidEvent.class, orderPaidSmsNotifyHandle);
evtManager.registerSubscriber("points-grant-on-order-paid", OrderPaidEvent.class, orderPaidPointsGrantHandle);
}
}6. 与属性计算(类型转换)的边界
第三类与第四类都属"领域层要一个产出、应用层给实现",易混淆,区分准则:
| 维度 | 属性计算(ATTRIBUTE_CALCULATOR) | 能力供给(CAPABILITY_PROVIDER) |
|---|---|---|
| 输入 | 至少一个领域对象/值对象/上下文 | 往往无输入,或仅简单领域参数 |
| 产出性质 | 由输入"推导"出的属性值/视图对象 | 新"产生"的标识符或领域对象(非由输入推导) |
| 典型方法 | calculate(T, E) / calculate(List<OrderItem>) | generate() / nextId() / createXxx(...) |
| 可替换为纯领域逻辑吗 | 往往可以(纯计算) | 通常不行(依赖基础设施才能落地) |
区分准则:输出是"算出来的"归第三类,"凭空造出来的(或需外部设施才能造出来的)"归第四类。
7. 命名规范速查
7.1 领域层(接口,以 I 开头)
| 类型 | 命名格式 | 示例 |
|---|---|---|
| 事件订阅 | I{事件}{业务动作意图}Handle | IOrderPaidSmsNotifyHandle |
| 校验规则 | I{业务对象}{具体规则意图}Rule | IOrderAmountLimitRule |
| 属性计算 | I{输入}To{输出}Converter / I{结果}Calculator | IOrderTotalCalculator |
| 能力供给 | I{产物}Generator / I{产物}Provider | IOrderIdGenerator |
7.2 应用层(实现,镜像接口名去 I)
| 类型 | 命名格式 | 示例 |
|---|---|---|
| 事件订阅 | 接口名去 I | OrderPaidSmsNotifyHandle |
| 校验规则 | 与接口同名(去 I) | OrderAmountLimitRule |
| 属性计算 | 与接口同名(去 I) | OrderTotalCalculator |
| 能力供给 | 与接口同名(去 I) | OrderIdGenerator |
命名中的"业务意图"指领域层承诺的具体领域动作(发短信 / 发积分 / 物化 ES 副本 / 金额上限 / 生成 ID),而非技术占位词。接口名本身即成为领域文档。
8. 包结构建议
domain/
└── {bounded-context}/
├── model/ # 聚合根、实体、值对象
├── event/ # 领域事件定义
├── dependency/ # 外部依赖端口(IDependency 子接口)
└── service/ # 领域服务接口定义(仅接口!)
├── IOrderPaidSmsNotifyHandle.java # 事件订阅契约
├── IOrderDataSyncEsProjectionHandle.java # 事件订阅契约(读模型副本)
├── IOrderAmountLimitRule.java # 校验规则契约
├── IOrderTotalCalculator.java # 属性计算契约
└── IOrderIdGenerator.java # 能力供给契约
application/
└── {bounded-context}/
├── command/ # 命令应用服务
├── query/ # 查询应用服务
├── subscriber/ # 事件订阅绑定({聚合}EventSubscriberRegistry)
│ └── OrderEventSubscriberRegistry.java
└── service/ # 领域服务实现
├── OrderPaidSmsNotifyHandle.java
├── OrderDataSyncEsProjectionHandle.java
├── OrderAmountLimitRule.java
├── OrderTotalCalculator.java
└── OrderIdGenerator.java9. 关键机制与避坑指南
9.1 category() 的注解查找链
category() 通过 findAnnotation 递归查找 @DomainService:实现类 → 所实现接口 → 父类。因此注解标注在实现类上,子类接口同样能读到分类;但若在领域层接口上标注、category() 由框架以接口类型调用时也能命中。
重要约束:
category()仅读取@DomainService注解。若契约接口既未标注注解、实现类也未标注,则category()恒返回UNKNOWN,该服务不会进入任何分类维度(不影响方法调用,但会丢失分类元信息,导致依赖分类的扫描/校验逻辑无法识别它)。
9.2 第三类标记子接口
重要约束:第三类属性计算已提供专属标记子接口
IAttributeCalculatorService(io.pragmatic.ddd.service包):自由计算契约继承它即可声明分类;绑定实体属性的场景复用io.pragmatic.ddd.base.IEntityPropertyCalculator<T,E,R>。注意框架未提供ITypeConverterService,误引入该不存在的接口会导致编译失败。
9.3 参数类型边界
重要约束:第二、三类契约的方法参数必须是领域类型(领域对象、值对象、领域上下文)。不得使用应用层
Command/HttpRequest/ DTO 作为方法入参,否则会破坏领域层对基础设施的零依赖约束,并使契约无法在领域层独立单测。
9.4 事件订阅的注册路径
重要约束:事件订阅实现(
IHandle<T>)必须显式调用IEventRegistry.registerSubscriber才会生效,框架不扫描IHandle实现,也不提供 Spring Boot 自动装配做这件事。未注册时事件发布后不会触发handleEvent,且没有日志或异常提示。注册以事件类的简单类名为路由 key(
subscribedToEventType().getSimpleName()),同一事件下的订阅者别名不可重复(重复抛IllegalArgumentException("<alias> is duplication"))。执行顺序不按注册顺序,需顺序保证时用dependSubscriber重载显式声明。
10. 向后兼容
- 现有实现若不标注
@DomainService,category()默认返回UNKNOWN,方法调用行为不受影响。 - 新契约继承对应基类接口并标注
@DomainService,即获得明确分类与描述元信息。 @DomainService为RUNTIME保留策略,仅增加元数据,不改方法契约,不影响运行期调用。
11. 总结速查
| 概念 | 基类接口(包) | 形态标志 | 方法形态 | 最关键的约束 |
|---|---|---|---|---|
事件订阅 EVENT_SUBSCRIBER | IEventSubscriberService<T>(service) | extends IHandle<T> | void handleEvent(T) | 仅响应已发生事件;必须显式 registerSubscriber,框架不扫描 |
业务规则 BUSINESS_RULE | ICheckRuleService<T>(service) | 返回 RuleCheckResult;同时是 ICheckRule<T> 子类型 | RuleCheckResult check(...) | 入参须为领域类型;只判断不改状态不写库 |
属性计算 ATTRIBUTE_CALCULATOR | IAttributeCalculatorService(service);绑定实体属性时复用 IEntityPropertyCalculator(base) | calculate(...) | R calculate(...) / R calculate(T,E) | 无 ITypeConverterService;输出须由输入推导 |
能力供给 CAPABILITY_PROVIDER | ICapabilityProviderService(service) | 产出方法 | T generate() / nextId() | 通常依赖基础设施;输出非由输入推导 |
未分类 UNKNOWN | — | 未标注 @DomainService | 任意 | category() 恒返回 UNKNOWN,丢失分类元信息 |
下一步建议阅读:
- 业务规则引擎:聚合根上的细粒度不变量校验
- 领域事件:
IHandle与事件总线注册 - 应用服务:
execute()中集成校验规则领域服务 - 事件订阅领域服务落地模式:EVENT_SUBSCRIBER 的契约、实现与注册绑定
- 聚合业务规则(OrderRule 范式):校验规则的落地