Что такое доменное событие (Domain Event) в DDD?

Ответ

Доменное событие (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, который избавляет тебя от пиздопроебибны в коде, когда всё завязано на всём. Волнение ебать, когда осознаёшь мощь этого подхода.