领域事件与领域服务
使用领域事件和领域服务记录业务事实。
领域事件与领域服务 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。
捕获业务事实
并非所有行为都适合放在单个实体中。领域事件捕获已经发生的、有意义的业务事实(OrderPlaced),让系统的其他部分能够作出响应。领域服务承载不自然属于某个实体或值对象的领域逻辑。本课将展示这两者如何在不使聚合膨胀的情况下,让模型保持清晰地表达业务。
什么是领域事件
领域事件是领域中某件已发生事情的不可变记录,使用通用语言并以过去时命名:OrderPlaced、PaymentReceived、CustomerRelocated。它携带描述该事实的数据以及事实发生的时间。它是一种值对象:创建一次,永不改变。
<?php
final class OrderPlaced {
public function __construct(
public readonly string $orderId,
public readonly int $totalCents,
public readonly DateTimeImmutable $occurredOn = new DateTimeImmutable()
) {}
}
$e = new OrderPlaced('o1', 1300);
echo $e->orderId, ' @ ', $e->occurredOn->format('H:i'), PHP_EOL;
聚合记录事件
聚合根会在自身行为的一部分中记录事件,然后公开这些事件,以便应用层在事务提交之后分发它们。聚合本身不负责分发——它只负责记录。这样可以使领域不受基础设施关注点影响。
<?php
final class OrderPlaced {
public function __construct(public readonly string $orderId) {}
}
abstract class AggregateRoot {
private array $events = [];
protected function record(object $e): void { $this->events[] = $e; }
public function releaseEvents(): array {
$out = $this->events; $this->events = []; return $out;
}
}
final class Order extends AggregateRoot {
public function __construct(public readonly string $id) {
$this->record(new OrderPlaced($id));
}
}
$o = new Order('o1');
foreach ($o->releaseEvents() as $e) { echo get_class($e), PHP_EOL; }
提交后分发
应用服务持久化聚合、提交事务,然后分发已释放的事件。提交后分发可以确保处理程序永远不会响应已回滚的事实。下面的顺序是处理电子邮件或外部调用等副作用时的安全模板。
<?php
final class PlaceOrderService {
public function __construct(
private object $repo,
private object $dispatcher
) {}
public function handle(object $order): void {
// 1. persist inside a transaction
$this->repo->save($order); // commit happens here
// 2. only AFTER commit, release and dispatch
foreach ($order->releaseEvents() as $event) {
$this->dispatcher->dispatch($event);
}
}
}
echo "order persisted, then events dispatched", PHP_EOL;
处理程序作出响应
每个事件都可以拥有多个处理程序,并且这些处理程序与触发事件的聚合相互解耦。PSR-14 分发器会将事件路由到已注册的监听器。添加新的响应(分析、通知)只需添加监听器,无需接触聚合。
<?php
final class OrderPlaced { public function __construct(public readonly string $orderId) {} }
final class Dispatcher {
private array $listeners = [];
public function on(string $event, callable $l): void { $this->listeners[$event][] = $l; }
public function dispatch(object $event): void {
foreach ($this->listeners[$event::class] ?? [] as $l) { $l($event); }
}
}
$d = new Dispatcher();
$d->on(OrderPlaced::class, fn($e) => print("email for {$e->orderId}\n"));
$d->on(OrderPlaced::class, fn($e) => print("analytics for {$e->orderId}\n"));
$d->dispatch(new OrderPlaced('o1'));
跨边界事件
领域事件将必须保持一致、但不能共享同一事务的聚合解耦开来。销售上下文中的OrderPlaced可以通过事件触发另一个上下文中的库存预留。在内部,事件通常是对象;跨服务时,它们通常会成为消息代理上的集成事件。请区分内部领域事件和外部集成事件——公共契约不应当等同于您的内部模型。
什么是领域服务
领域服务承载满足以下条件的领域逻辑:
- 涉及多个聚合或实体,因此不适合放入其中任何一个;并且
- 它确实是一个领域概念,并以通用语言表达。
它是无状态的,并且作用于领域对象。负责从一个账户扣款并向另一个账户入账的FundsTransfer服务就是经典示例。
代码中的领域服务
该服务协调两个聚合,但不持有自己的状态。由于这段逻辑不属于任何一个账户,因此它位于以领域操作命名的服务中。
<?php
final class Account {
public function __construct(public readonly string $id, private int $cents) {}
public function withdraw(int $c): void {
if ($c > $this->cents) { throw new DomainException('insufficient funds'); }
$this->cents -= $c;
}
public function deposit(int $c): void { $this->cents += $c; }
public function balance(): int { return $this->cents; }
}
final class MoneyTransferService {
public function transfer(Account $from, Account $to, int $cents): void {
$from->withdraw($cents);
$to->deposit($cents);
}
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new MoneyTransferService())->transfer($a, $b, 400);
echo $a->balance(), '/', $b->balance(), PHP_EOL; // 600/400
领域服务与应用服务
不要混淆二者:
- 领域服务包含业务逻辑,位于领域层;它不了解事务或超文本传输协议。
- 应用服务负责协调一个用例:从仓储加载聚合,调用领域逻辑,提交事务,并分发事件。它是一个不包含自身业务规则的轻量协调器。
避免服务陷阱
领域服务很容易被过度使用。如果逻辑可以放在实体或值对象上,就应该这样做:这是避免贫血模型的方法。只有当操作确实跨越多个聚合,或没有自然归属时,才应使用领域服务。否则,您会抽空实体中的行为,并重新构造出程序式代码。
应用服务负责协调
综合起来:精简的应用服务从仓储中加载聚合,调用领域服务处理跨聚合逻辑,持久化结果并派发事件。它只负责编排,不包含自身的业务规则。
<?php
final class Account {
public function __construct(public readonly string $id, public int $cents) {}
}
final class MoneyTransferService { // domain service: the rule
public function transfer(Account $from, Account $to, int $c): void {
if ($c > $from->cents) { throw new DomainException('insufficient'); }
$from->cents -= $c; $to->cents += $c;
}
}
final class TransferApp { // application service: orchestration only
public function __construct(private MoneyTransferService $domain) {}
public function run(Account $a, Account $b, int $c): void {
$this->domain->transfer($a, $b, $c); // then repo->save + dispatch
}
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new TransferApp(new MoneyTransferService()))->run($a, $b, 250);
echo $a->cents, '/', $b->cents, PHP_EOL; // 750/250
快速检查
事件与服务。
回顾
您已经捕获了业务事实和无处归属的逻辑。领域事件是不可变的过去时记录,由聚合记录,再由应用层在提交后派发,从而解耦后续反应并跨越聚合边界。领域服务承载横跨多个聚合的无状态领域逻辑,与仅负责编排用例的精简应用服务不同。请谨慎使用这两者,让行为尽可能留在实体和值对象中应有的位置。
常见问题解答
「领域事件与领域服务」课时是免费的吗?
是的 — 「领域事件与领域服务」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。
「领域事件与领域服务」这节课中我会学到什么?
使用领域事件和领域服务记录业务事实。 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 PHP Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「领域事件与领域服务」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 PHP Academy 课中编写并运行代码吗?
能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- DDD 构建模块:实体与值对象
- 聚合、仓储与工厂
- 领域事件与领域服务
- 限界上下文与上下文映射