本文目录导读:

- 📚 目录导读
- 核心问题:跨聚合逻辑为何是DDD的痛点?
- 领域服务 vs 应用服务:职责边界如何划分?
- PHP实现跨聚合逻辑的三种推荐模式
- 实战代码:领域服务处理跨订单与库存聚合
- 常见陷阱与架构避坑指南
- 问答环节:你关心的高频问题
- 总结:何时该用领域服务,何时该用领域事件?
PHP领域服务:如何优雅处理跨聚合逻辑?——DDD实践指南
📚 目录导读
- 核心问题:跨聚合逻辑为何是DDD的痛点?
- 领域服务 vs 应用服务:职责边界如何划分?
- PHP实现跨聚合逻辑的三种推荐模式
- 实战代码:领域服务处理跨订单与库存聚合
- 常见陷阱与架构避坑指南
- 问答环节:你关心的高频问题
- 何时该用领域服务,何时该用领域事件?
核心问题:跨聚合逻辑为何是DDD的痛点?
在领域驱动设计(DDD)中,聚合(Aggregate) 被设计为数据一致性的边界,一个聚合内的对象通过根实体保证事务一致性,而跨聚合的操作天然面临“分布式”或“最终一致性”的挑战。
典型场景:
- 处理订单时需同时扣减库存(订单聚合+库存聚合)
- 用户注册后自动创建默认优惠券(用户聚合+优惠券聚合)
- 转账业务涉及两个账户聚合的余额变更
核心矛盾:
聚合本应保持自治,但业务逻辑又不得不让它们协同,此时若简单将逻辑塞入某个聚合,会破坏其边界;若写在应用服务层,又会导致业务规则散失。
DDD的答案:领域服务(Domain Service)
领域服务 vs 应用服务:职责边界如何划分?
在PHP架构中,两者常被混淆,以下是关键区别:
| 维度 | 领域服务 | 应用服务 |
|---|---|---|
| 所在层 | 领域层 | 应用层 |
| 职责 | 承载无状态、跨聚合的业务规则 | 协调外部资源、事务、权限 |
| 是否访问仓储 | 是(通过接口) | 是(直接依赖) |
| 可复用性 | 可被多个应用服务调用 | 特定用例专用 |
| 例子 | OrderFulfillmentService |
PlaceOrderController |
典型反例:
将跨聚合逻辑写在应用服务中,会导致:
// 反例:应用服务直接操作两个仓储并处理业务规则
class PlaceOrderUseCase {
public function execute(...) {
$order = $this->orderRepo->save(...);
$inventory = $this->inventoryRepo->find(...);
if ($inventory->getQuantity() < $order->getQuantity()) {
throw new Exception('库存不足'); // 业务规则泄露到应用层
}
$inventory->decrease($order->getQuantity());
$this->inventoryRepo->save($inventory);
}
}
正确做法:将库存校验与扣减封装到领域服务中。
PHP实现跨聚合逻辑的三种推荐模式
模式1:领域服务 + 事务脚本(适用简单跨聚合)
class OrderCheckoutService
{
public function __construct(
private OrderRepositoryInterface $orderRepo,
private InventoryRepositoryInterface $inventoryRepo
) {}
public function checkout(string $orderId, string $productId): void
{
$order = $this->orderRepo->findOrFail($orderId);
$inventory = $this->inventoryRepo->findOrFail($productId);
if (! $inventory->canReserve($order->getQuantity())) {
throw new InsufficientStockException();
}
// 执行跨聚合操作
$order->markAsCheckedOut();
$inventory->reserve($order->getQuantity());
$this->orderRepo->save($order);
$this->inventoryRepo->save($inventory);
}
}
注意:此模式需开发者自行管理事务(例如在应用层开启数据库事务)。
模式2:领域服务 + 领域事件(推荐复杂场景)
利用消息机制解耦跨聚合,使各个聚合独立更新:
- 订单聚合触发
OrderPlacedEvent - 库存聚合监听事件后扣减库存
- 通过PHP事件调度器(如Symfony EventDispatcher)实现
模式3:领域服务 + 聚合服务
为每个聚合定义一个“服务接口”,由领域服务协调:
class FulfillmentService implements FulfillmentServiceInterface
{
public function fulfill(Order $order, Inventory $inventory): void
{
$inventory->decrease($order->getQuantity());
$order->setStatus('fulfilled');
// 可触发更多聚合更新
}
}
实战代码:领域服务处理跨订单与库存聚合
以下是一个完整的PHP开发者可直接运行的例子(基于Symfony风格):
// 领域层:领域服务
class OrderInventoryService
{
public function __construct(
private OrderRepository $orderRepo,
private InventoryRepository $inventoryRepo
) {}
public function reserveProducts(OrderId $orderId): void
{
$order = $this->orderRepo->findOrFail($orderId);
$productId = $order->getProductId();
$quantity = $order->getQuantity();
$inventory = $this->inventoryRepo->findOrFail($productId);
if (! $inventory->hasAvailable($quantity)) {
throw new DomainException('库存不足,当前可售: ' . $inventory->getAvailableStock());
}
$inventory->decrease($quantity);
$order->markAsStockReserved();
// 使用Unit of Work模式保证最终一致性
$this->orderRepo->save($order);
$this->inventoryRepo->save($inventory);
}
}
// 应用层:调用领域服务
class PlaceOrderController
{
public function __invoke(Request $request): Response
{
// 验证请求...
$orderId = new OrderId($request->get('order_id'));
try {
$this->orderInventoryService->reserveProducts($orderId);
} catch (DomainException $e) {
return new JsonResponse(['error' => $e->getMessage()], 400);
}
// 然后可以触发领域事件...
return new JsonResponse(['status' => 'ok']);
}
}
常见陷阱与架构避坑指南
-
陷阱1:在领域服务中直接使用ORM实体
✅ 解决方案:只依赖仓储接口,不直接new实体。 -
陷阱2:领域服务变身“上帝类”
✅ 每个领域服务只处理一类跨聚合逻辑(如PaymentService不混入发货逻辑)。 -
陷阱3:丢失业务语义
✅ 方法名用业务术语:checkout()比saveAndUpdateStock()更清晰。 -
陷阱4:事务边界过大
✅ 为跨聚合操作设计独立的事务脚本,避免锁住多个聚合。 -
陷阱5:忽视最终一致性
✅ 对非关键路径(如发送邮件),采用事件驱动,不必强事务。
问答环节:你关心的高频问题
Q1:领域服务一定需要注入仓储吗?
A: 当它需要持久化数据变化时,必须依赖仓储,如果只进行校验或计算,可以不依赖(例如PricingService)。
Q2:跨聚合逻辑可否用聚合内的领域事件直接处理?
A: 可以,但要注意:事件监听器也应属于领域层,而不是应用层,PHP中常配合Symfony/Event或商业框架的消息总线。
Q3:使用领域服务后,应用服务还做什么?
A: 应用服务负责:鉴权、事务开启/提交、输入验证、调用领域服务、组装DTO、处理响应,它不应包含业务规则。
Q4:小项目是否强制用领域服务?
A: 不建议,如果你的跨聚合逻辑简单且变更频率低,直接写在应用层也能接受,领域服务适用于业务复杂、变化频繁的中大型系统。
Q5:PHP性能对领域服务模式有影响吗?
A: 无显著影响,领域服务本质是类实例化与调用,只要避免过度设计,性能问题常见于I/O(如多次仓储查询)而非模式本身。
何时该用领域服务,何时该用领域事件?
| 场景 | 推荐方式 |
|---|---|
| 两个聚合需要强一致事务(如扣库存+创建订单) | 领域服务 + 数据库事务 |
| 跨聚合操作可容忍短期不一致(如用户注册后发欢迎邮件) | 领域事件 + 异步处理 |
| 同一业务操作影响3个以上聚合 | 考虑将部分逻辑转为事件或CQRS |
| 需要业务规则校验(如“用户VIP可超额下单”) | 领域服务封装校验逻辑 |
最终建议:
先看业务有没有原子性要求,再决定用领域服务还是领域事件,PHP中,多数跨聚合逻辑建议先写在领域服务中,当性能或复杂度提升后,再逐步解耦。
领域服务不是万能药,滥用会导致贫血领域模型,它应该像客厅的插座——只在需要的地方出现,当你发现跨聚合逻辑越来越像“规则”而非“协调”时,就是使用它的最佳时机。