Шаг первый: понимание сути. Domain Event — это факт, что что-то произошло в домене, и это что-то важно для других частей системы. Ключевое слово — «факт». Событие фиксирует то, что уже случилось, его нельзя отменить. Примеры: `OrderPlaced` (Заказ размещен), `InvoiceIssued` (Счет выставлен), `UserEmailVerified` (Email пользователя подтвержден). Событие именуется в прошедшем времени и содержит данные, релевантные на момент его возникновения.
Шаг второй: проектирование события. Хорошее Domain Event должно быть атомарным, нести минимальный, но достаточный набор данных и быть неизменяемым. Рассмотрим пример на TypeScript для системы электронной коммерции. Событие `OrderPlaced` может выглядеть так:
```
class OrderPlaced implements DomainEvent {
public readonly occurredOn: Date;
constructor(
public readonly orderId: string,
public readonly customerId: string,
public readonly totalAmount: number,
public readonly lineItems: Array
) {
this.occurredOn = new Date();
}
}
```
Обратите внимание: класс содержит только данные, необходимые для реакции на факт размещения заказа. Мы не включаем сюда всю модель заказа со всеми полями.
Шаг третий: публикация события. Событие должно публиковаться из агрегата (Aggregate Root) в момент совершения значимого действия. Важно публиковать событие *после* того, как изменения в агрегате успешно сохранены (в рамках транзакции), чтобы избежать inconsistency. В чистой архитектуре это часто делается через коллекцию событий внутри агрегата. Пример метода в агрегате `Order`:
```
class Order extends AggregateRoot {
private _lineItems: OrderLineItem[] = [];
// ... другие поля
public place(customerId: string) {
// Бизнес-логика проверки...
this.status = OrderStatus.PLACED;
this.addDomainEvent(new OrderPlaced(
this.id,
customerId,
this.calculateTotal(),
this._lineItems.map(li => ({ productId: li.productId, quantity: li.quantity }))
));
}
public getDomainEvents(): DomainEvent[] { /* возврат накопленных событий */ }
public clearDomainEvents(): void { /* очистка коллекции */ }
}
```
Шаг четвертый: диспетчеризация и обработка. После сохранения агрегата в репозитории, события извлекаются и отправляются в диспетчер (Event Bus). Диспетчер отвечает за доставку события всем зарегистрированным обработчикам (Event Handlers). Обработчик — это use case или сервис, который выполняет побочный эффект в ответ на событие. Продолжим пример: после `OrderPlaced` может быть несколько обработчиков:
- `SendOrderConfirmationEmailHandler`: отправляет письмо клиенту.
- `UpdateInventoryHandler`: резервирует товары на складе.
- `NotifyWarehouseHandler`: отправляет уведомление на склад для сборки.
Шаг пятый: интеграция с инфраструктурой. На практике для асинхронной обработки событий между контекстами (Bounded Context) используется брокер сообщений, например, RabbitMQ, Apache Kafka или облачные очереди (AWS SQS, Google Pub/Sub). Событие сериализуется (например, в JSON) и помещается в очередь. Другие микросервисы или модули подписываются на эти очереди. Это позволяет добиться слабой связанности и отказоустойчивости. Важно разработать схему (schema) для событий и версионировать их для обеспечения обратной совместимости.
Шаг шестой: обработка ошибок и компенсирующие действия. Что если обработчик `UpdateInventoryHandler` не смог зарезервировать товар из-за его отсутствия? Простое отбрасывание события недопустимо. Необходима стратегия повторных попыток (retry policy) с экспоненциальной задержкой. В случае персистентной ошибки событие должно попадать в «мертвую букву» очередь (DLQ) для ручного разбора. В сложных сценариях может потребоваться запуск компенсирующей транзакции (Saga), например, отмена заказа и отправка извинительного письма.
Практический пример: система бронирования отелей. Событие `RoomBooked` публикуется при успешном бронировании. Его обработчики: 1) списывают оплату с карты гостя (внешний платежный сервис), 2) блокируют номер в календаре, 3) начисляют бонусные баллы на счет гостя. Если платеж не прошел, запускается сага, которая отменяет блокировку номера и отменяет начисление баллов.
Внедрение Domain Events требует дисциплины и тщательного проектирования, но результат того стоит. Вы получаете систему, где бизнес-процессы явно выражены в коде, компоненты слабо связаны, а возможность масштабирования и добавления новой функциональности без переписывания существующего кода становится реальностью.
Комментарии (11)