Ответ
Доменное событие (Domain Event) — это неизменяемый объект-значение, который фиксирует факт, произошедший в предметной области (домене) и имеющий значение для бизнеса. Он представляет собой что-то, что уже случилось в прошлом, например, OrderWasPlaced, UserEmailWasChanged или PaymentFailed. События используются для информирования других частей системы о произошедшем изменении, способствуя слабой связанности между агрегатами и контекстами.
Характеристики доменного события:
- Именование: Имя события — глагол в прошедшем времени, отражающий завершённое действие.
- Неизменяемость (Immutability): После создания данные события не могут быть изменены.
- Содержание: Содержит минимальный набор данных, необходимый для обработки события (например, ID агрегата, временная метка, релевантные свойства).
- Контекст: Привязано к конкретному агрегату, который его породил.
Пример реализации на PHP:
<?php
declare(strict_types=1);
namespace AppDomainOrderEvent;
use AppDomainOrderValueObjectOrderId;
use AppDomainSharedEventDomainEvent;
use DateTimeImmutable;
final class OrderWasPlaced implements DomainEvent
{
private DateTimeImmutable $occurredOn;
public function __construct(
private OrderId $orderId,
private string $customerEmail,
private float $totalAmount
) {
$this->occurredOn = new DateTimeImmutable();
}
// Геттеры предоставляют доступ к данным события
public function getOrderId(): OrderId
{
return $this->orderId;
}
public function getCustomerEmail(): string
{
return $this->customerEmail;
}
public function getTotalAmount(): float
{
return $this->totalAmount;
}
public function occurredOn(): DateTimeImmutable
{
return $this->occurredOn;
}
}
Как это работает внутри агрегата (Order):
class Order extends AggregateRoot
{
private OrderId $id;
private array $recordedEvents = [];
public static function place(OrderId $id, string $customerEmail, array $items): self
{
$order = new self();
$order->id = $id;
// ... логика создания заказа ...
// 1. После успешного создания агрегата регистрируем событие
$order->recordThat(new OrderWasPlaced($id, $customerEmail, $order->calculateTotal()));
return $order;
}
private function recordThat(DomainEvent $event): void
{
$this->recordedEvents[] = $event;
}
// 2. Метод для извлечения и очистки записанных событий (вызывается после сохранения агрегата)
public function releaseEvents(): array
{
$events = $this->recordedEvents;
$this->recordedEvents = [];
return $events;
}
}
Практическое применение: После сохранения агрегата Order в репозитории, все записанные события (OrderWasPlaced) извлекаются и отправляются в шину событий. На них могут реагировать различные обработчики:
- Отправка email-подтверждения заказа.
- Обновление аналитической панели о новых продажах.
- Резервирование товара на складе.
Это позволяет разделить ответственность и избежать прямых вызовов сервисов внутри доменной логики.
Ответ 18+ 🔞
А, слушай, вот эта тема с доменными событиями — это вообще огонь, когда до тебя доходит, как оно работает. Раньше же все писали код, где один сервис вызывает другой, тот третий, и получается такая спагеттина, что терпения ноль ебать. А потом бац — и узнаёшь про события.
Представь себе, твой заказ в системе — это как агрегат, такой самодостаточный мужик. Он не бегает сам и не кричит другим сервисам: «Эй, я создался, иди сюда, отправь письмо!». Он просто фиксирует факт: «Так, блядь, заказ был оформлен. Всё. Дело сделано». И записывает этот факт у себя в блокнотик, как внутреннее событие OrderWasPlaced.
Вот в чём прикол: это событие — неизменяемый объект. То есть, создал — и хуй с горы, назад не откатишь. Оно как констатация: «Да, чувак, вот это вот случилось». Имя у него всегда в прошедшем времени, потому что это уже история.
А дальше начинается магия. После того как этот агрегат (заказ) сохранили в базу, у него спрашивают: «Ну что, какие у тебя там события накопились?». Он такой: «Да вот, одно событие, OrderWasPlaced». И это событие вытаскивают и кидают в шину событий. Это как крикнуть в коридор: «Ребята, заказ оформили!».
И тут начинается ёперный театр. На этот крик из разных комнат выскакивают обработчики:
- Один — чтобы письмо клиенту отправить.
- Второй — чтобы в аналитику статистику запилить.
- Третий — чтобы на складе товар зарезервировать.
И все они работают параллельно и независимо. Главный агрегат про них нихуя не знает. Он свою работу сделал — заказ создал и факт зафиксировал. А дальше — да похуй, кто и что с этим фактом сделает. Связь-то слабая получается, красота!
Смотри на код, тут всё просто:
final class OrderWasPlaced implements DomainEvent
{
private DateTimeImmutable $occurredOn;
public function __construct(
private OrderId $orderId,
private string $customerEmail,
private float $totalAmount
) {
$this->occurredOn = new DateTimeImmutable(); // Время фиксируется при создании
}
// ... геттеры ...
}
Объект события — просто пачка данных (ID, email, сумма) и метка времени. Никакой логики внутри, только данные. Создали — и забыли.
А вот как это внутри агрегата рождается:
class Order extends AggregateRoot
{
private array $recordedEvents = [];
public static function place(OrderId $id, string $customerEmail, array $items): self
{
$order = new self();
// ... тут какая-то бизнес-логика, проверки ...
// Всё прошло ок? Регистрируем событие!
$order->recordThat(new OrderWasPlaced($id, $customerEmail, $order->calculateTotal()));
return $order;
}
private function recordThat(DomainEvent $event): void
{
$this->recordedEvents[] = $event; // Кинул в свой внутренний список
}
// Потом этот метод вызовет репозиторий после сохранения
public function releaseEvents(): array
{
$events = $this->recordedEvents;
$this->recordedEvents = []; // Очистил список
return $events; // Отдал на растерзание шине
}
}
И всё! Агрегат не отправляет письма, не лезет в базу аналитики. Он только заявляет о факте. А система вокруг уже решает, что с этим фактом делать. Это как раз и есть тот самый нормальный, годный DDD, который избавляет тебя от пиздопроебибны в коде, когда всё завязано на всём. Волнение ебать, когда осознаёшь мощь этого подхода.