PHP领域服务处理跨聚合逻辑吗

wen PHP项目 1

本文目录导读:

PHP领域服务处理跨聚合逻辑吗

  1. 📚 目录导读
  2. 核心问题:跨聚合逻辑为何是DDD的痛点?
  3. 领域服务 vs 应用服务:职责边界如何划分?
  4. PHP实现跨聚合逻辑的三种推荐模式
  5. 实战代码:领域服务处理跨订单与库存聚合
  6. 常见陷阱与架构避坑指南
  7. 问答环节:你关心的高频问题
  8. 总结:何时该用领域服务,何时该用领域事件?

PHP领域服务:如何优雅处理跨聚合逻辑?——DDD实践指南

📚 目录导读

  1. 核心问题:跨聚合逻辑为何是DDD的痛点?
  2. 领域服务 vs 应用服务:职责边界如何划分?
  3. PHP实现跨聚合逻辑的三种推荐模式
  4. 实战代码:领域服务处理跨订单与库存聚合
  5. 常见陷阱与架构避坑指南
  6. 问答环节:你关心的高频问题
  7. 何时该用领域服务,何时该用领域事件?

核心问题:跨聚合逻辑为何是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. 陷阱1:在领域服务中直接使用ORM实体
    ✅ 解决方案:只依赖仓储接口,不直接new实体。

  2. 陷阱2:领域服务变身“上帝类”
    ✅ 每个领域服务只处理一类跨聚合逻辑(如PaymentService不混入发货逻辑)。

  3. 陷阱3:丢失业务语义
    ✅ 方法名用业务术语:checkout()saveAndUpdateStock()更清晰。

  4. 陷阱4:事务边界过大
    ✅ 为跨聚合操作设计独立的事务脚本,避免锁住多个聚合。

  5. 陷阱5:忽视最终一致性
    ✅ 对非关键路径(如发送邮件),采用事件驱动,不必强事务。


问答环节:你关心的高频问题

Q1:领域服务一定需要注入仓储吗?
A: 当它需要持久化数据变化时,必须依赖仓储,如果只进行校验或计算,可以不依赖(例如PricingService)。

Q2:跨聚合逻辑可否用聚合内的领域事件直接处理?
A: 可以,但要注意:事件监听器也应属于领域层,而不是应用层,PHP中常配合Symfony/Event或商业框架的消息总线。

Q3:使用领域服务后,应用服务还做什么?
A: 应用服务负责:鉴权、事务开启/提交、输入验证、调用领域服务、组装DTO、处理响应,它不应包含业务规则。

Q4:小项目是否强制用领域服务?
A: 不建议,如果你的跨聚合逻辑简单且变更频率低,直接写在应用层也能接受,领域服务适用于业务复杂、变化频繁的中大型系统。

Q5:PHP性能对领域服务模式有影响吗?
A: 无显著影响,领域服务本质是类实例化与调用,只要避免过度设计,性能问题常见于I/O(如多次仓储查询)而非模式本身。


何时该用领域服务,何时该用领域事件?

场景 推荐方式
两个聚合需要强一致事务(如扣库存+创建订单) 领域服务 + 数据库事务
跨聚合操作可容忍短期不一致(如用户注册后发欢迎邮件) 领域事件 + 异步处理
同一业务操作影响3个以上聚合 考虑将部分逻辑转为事件或CQRS
需要业务规则校验(如“用户VIP可超额下单”) 领域服务封装校验逻辑

最终建议:
先看业务有没有原子性要求,再决定用领域服务还是领域事件,PHP中,多数跨聚合逻辑建议先写在领域服务中,当性能或复杂度提升后,再逐步解耦。

领域服务不是万能药,滥用会导致贫血领域模型,它应该像客厅的插座——只在需要的地方出现,当你发现跨聚合逻辑越来越像“规则”而非“协调”时,就是使用它的最佳时机。

抱歉,评论功能暂时关闭!