Domain Service / Domain Event(领域服务与领域事件)
解决什么问题:有些业务逻辑不属于任何聚合根,跨多个聚合——需要 Domain Service;有些业务状态变化需要通知其他 BC——需要 Domain Event。两者都是聚合根之间的"协调机制"。
它是什么
Domain Service(领域服务):不属于任何聚合根的业务操作——封装跨聚合、跨实体的业务规则。
Domain Event(领域事件):过去发生的业务事实——发布后被订阅者消费,触发后续业务逻辑。
Vernon Ch 7: "A SERVICE is an operation offered by the domain that does not naturally fit into any ENTITY or VALUE OBJECT."
Vernon Ch 8: "Domain Events are domain occurrences that domain experts care about."
V2 视角(V2.7 B3 段 6 改造后):Domain Service = 跨聚合的控制律(调用多个聚合完成业务),Domain Event = 跨 BC 的状态广播(解耦发布者与订阅者)。
关键属性
| 属性 | Domain Service | Domain Event |
|---|---|---|
| 位置 | 领域层,独立类 | 由聚合根 / Domain Service 发布 |
| 状态 | 无状态(或会话状态) | 不可变(已发生的事实) |
| 命名 | 业务动词(TransferMoney) | 过去式(MoneyTransferred) |
| 输入 | 业务参数(聚合根、值对象) | 事件 ID、时间戳、聚合根 ID、关键字段 |
| 输出 | 业务结果(可能是新聚合根、事件) | 仅持久化 + 发布到 EventBus |
| 反模式 | 写成 Application Service 风格(无状态包装) | 写成命令("CreateOrder" 是命令,"OrderCreated" 是事件) |
| 核心约束 | 不操作持久化、不调用 Repository(除非必要) | 不可变、不可取消、不可删除 |
怎么用
场景 1:Domain Service(跨聚合业务规则)
场景:转账——一个聚合根(SourceAccount)扣钱,另一个聚合根(TargetAccount)加钱。
class TransferService: # Domain Service
def transfer(self, source_id, target_id, amount):
# 1. 加载两个聚合根
source = account_repo.find_by_id(source_id)
target = account_repo.find_by_id(target_id)
# 2. 业务规则(不属于任何单方)
if not source.can_debit(amount):
raise InsufficientFunds()
# 3. 调用聚合根方法
source.debit(amount)
target.credit(amount)
# 4. 发布领域事件
self.event_bus.publish(MoneyTransferred(source_id, target_id, amount))场景 2:Domain Event(状态广播)
场景:Order 聚合根状态变更(已支付),通知 Inventory BC(扣库存)和 Shipping BC(创建运单)。
class Order:
def mark_paid(self, payment_id):
self.status = "PAID"
self.add_event(OrderPaid(self.id, payment_id))
class OrderPaid: # Domain Event
def __init__(self, order_id, payment_id):
self.order_id = order_id
self.payment_id = payment_id
self.occurred_at = now()
# 订阅者(Inventory BC)
class InventoryHandler:
def handle(self, event: OrderPaid):
# 反查订单明细 + 扣库存
...场景 3:Domain Event + EventBus(解耦发布与订阅)
Order.mark_paid() → OrderPaid event
↓
┌──────────┴──────────┐
[EventBus] 发布到多个订阅者
↓ ↓ ↓
Inventory Shipping Notification
BC:扣库存 BC:创建运单 BC:发邮件发布者不知道订阅者存在——完全解耦。新增订阅者不需要改 Order 代码。
场景 4:Domain Service 与 Application Service 的区别
| 维度 | Domain Service | Application Service |
|---|---|---|
| 职责 | 业务规则 | 协调 / 事务 / 安全 |
| 依赖 | 聚合根 + Repository | Domain Service + 基础设施 |
| 层 | 领域层 | 应用层 |
| 测试 | 纯单元测试 | 集成测试 |
常见误区
- "Domain Service = Application Service"——错。Domain Service 在领域层(含业务规则),Application Service 在应用层(协调事务)。详见 应用 vs 领域服务
- "Domain Event 是命令"——错。Event 是过去式(OrderPaid),不是祈使句(PayOrder)。命令是 Command 模式,事件是 Event 模式
- "Event 应该可变"——错。Event 是已发生的事实,永远不可变。如果要"修改"事实 = 发新事件(OrderPaid → OrderRefunded)
- "Domain Service = 工具类"——错。Domain Service 有业务含义(TransferMoney 不是 Util),工具类是没有业务含义的辅助方法
- "事件 = 方法调用"——错。事件是异步广播,方法调用是同步请求。混用 = 强耦合。事件必须经过 EventBus 异步派发
- "事件可以乱序到达"——错。事件必须有因果顺序(OrderPaid 在 OrderCreated 之后)。用全局顺序号或聚合根 ID 哈希分片保证
关键洞察
Domain Service = 跨聚合的"控制律"——它不属于任何聚合根,但属于领域。
Domain Event = 跨 BC 的"广播"——它解耦发布与订阅。
两者协作模式:
- Domain Service 内部用聚合根的方法(保证不变量)
- 业务完成后发布 Domain Event(通知其他 BC)
- EventBus 异步派发到订阅者(不阻塞发布者)
这是 V2 B3 分解 + 控制论 Ch 7 "协调"的工程化——跨子系统的协作 = 控制律(Domain Service)+ 状态广播(Domain Event)。
相关链接
- Aggregate——Domain Service 操作的对象
- Entity & Value Object——区分领域对象 vs 领域服务
- Saga——Domain Event 驱动的长事务
- CQRS-ES——Domain Event 作为状态变更主路径
- [V3 涌现作为] · 分解——跨聚合的协调机制
- 源典 IDDD Ch 7 + Ch 8——Vernon 原书精读
- 源典 EDM Ch 2-3 + Ch 3——事件驱动架构基础