Реалізація базових подій у моделі предметної області
Мета: Ознайомитися з концепцією доменних подій. Реалізувати механізм публікації та обробки подій у предметній області.
Завдання¶
Реалізувати базові доменні події, які реагують на зміни в агрегатах/сутностях.
Як реалізовувати «Обробники»?
«Обробники» — це формальність, реалізовувати їх явно не потрібно.
Теоретичні відомості¶
Подія предметної області (Domain Event) — це повідомлення або об'єкт, що описує важливу подію, яка вже відбулася в бізнес-області.
Ключові характеристики:
- Минулий час: Назви подій завжди формулуються в минулому часі, оскільки змінити те, що вже сталося, неможливо;
- Незмінність (Immutabillity): Об'єкти подій не можна змінювати після створення;
- Публічний інтерфейс: Події є частиною контракту агрегату. Агрегат публікує їх, щоб дозволити іншим компонентам (підписникам) реагувати на зміни без прямої залежності.
Паблішер (Publisher) — Це механізм («гучномовець»), який відповідає за розсилку події. Його задача — взяти об'єкт події (наприклад OrderPlaced) і передати його всім зацікавленим компонентам системи. Паблішер не знає хто його слухає і що буде зроблено з подією — він працює за механізмом «відправив і забув».
Слухач (Listener) або Обробник (Hanbler) — Це компонент («підписник»), який чекає на конкретний тип події. Коли паблішер викидає подію, Слухач автоматично перехоплює її та виконує певну бізнес-логіку (побічний ефект), наприклад, відправляє email, записує лог або оновлює статистику.
Хід роботи¶
Ініціалізація Publisher¶
Враховуючи, що проєкт базується на стартерах spring-boot, було вирішено скористатися вже вбудованим механізмом паблішера, який реалізує патерн Observer. Нижче наведено код реалізації з проєкту:
Варіант 1¶
public class OrderPreferenceServiceImpl implements OrderPreferenceService {
private final ApplicationEventPublisher publisher;
@Override
public Order changeAddressDelivery(Order order, Address deliveryAddress) {
// Додається event зміни адреси на нову
order.changeAddressDelivery(deliveryAddress);
// Тут необхідно було зберегти ордер
// Публікую цей event
order.getEvents().forEach(publisher::publishEvent);
order.clearEvents();
return order;
}
}
public void changeAddressDelivery(Address address) {
if (status == OrderStatus.NEW) {
this.shippingAddress = address;
// Варіант 1: Додавання події зміни адреси з агрегату
this.events.add(new OrderChangedAddressDeliveryEvent(...);
return;
}
throw new IllegalArgumentException("Order status is not NEW");
}
Про компонент ApplicationEventPublisher
Повідомляє всіх слухачів, зареєстрованих у програмі, про подію. Події можуть бути як від фреймворку (наприклад, ContextRefreshedEvent), так і специфічними для програми.
Джерело: docs.spring.io
Метод changeAddressDelivery виконує бізнес-логіку, яка призводить до додавання у список івентів об'єкта, що представляє подію, в даному випадку це OrderChangedAddressDeliveryEvent. Після цього сервіс, будучи паблішером, передає у "потік" цю подію, яка буде застосована у наступному (сервісі) коді:
Варіант 2¶
Другий варіант відрізняється лише способом передачі івентів: якщо перший передбачає їх накопичення в агрегаті, то тут відбувається публікація одразу після виконання певної бізнес-логіки на рівні сервісу:
public class OrderPlacementServiceImpl implements OrderPlacementService {
private final ApplicationEventPublisher publisher;
@Transactional
@Override
public Order placeOrder(
Customer customer,
List<OrderItemDetails> items,
Map<UUID, Product> productRepo,
Address deliveryAddress) {
// 1. Валідація та Списання (Це має бути в одній транзакції!)
items.forEach(...);
// 2. Створення замовлення (Тут ми створюємо агрегат Order)
Order newOrder = Order.placeOrder(...);
// 3. Зв'язування з клієнтом (Змінюємо стан агрегата Customer)
customer.addOrder(newOrder);
// 4. (Варіант 2) Публікуємо подію розміщення замовлення у сервісі
publisher.publishEvent(new OrderPlacedEvent(...));
return newOrder;
}
}
Обидва варіанти можуть використовуватися для різних цілей: перший підходить для накопичення, наприклад, історії змін, а другий — для миттєвого реагування, як-от надсилання листа на пошту про помилку на сервері.
Обробники¶
У цьому розділі наведено код обробників, які використовуються для варіантів 1 та 2. Обидва викидають спеціальний виняток UnsupportedOperationException, оскільки їх не потрібно реалізовувати згідно із завданням.
Варіант 1
public class EmailServiceImpl implements EmailService {
@Override
public void send(String email, String message) {
throw new UnsupportedOperationException(
"Unsupported operation sending: %s | %s".formatted(...));
}
@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
this.send(
event.email(),
"[%s] Order placed, status: %s, by address: %s"
.formatted(...));
}
}
Варіант 2
public class AddressChangeListener {
// Слухач варіанту 2: спрацьовує при зміні адреси в ордері
@EventListener
public void onAddressUpdated(OrderChangedAddressDeliveryEvent event) {
throw new UnsupportedOperationException(
"Order [%s] changed delivery address: %s"
.formatted(...);
// Тут можна викликати зовнішні сервіси, відправити email тощо
}
}
Про анотацію @EventListener
Анотація, яка позначає метод як слухач подій програми. Якщо анотований метод підтримує один тип події, він може оголосити один параметр, який відображає тип події для прослуховування. Якщо анотований метод підтримує кілька типів подій, ця анотація може посилатися на один або кілька підтримуваних типів подій за допомогою атрибута classes . Див. classes()javadoc для отримання додаткової інформації.
Джерело: docs.spring.io
Висновок¶
Виконання практичної роботи передбачало ознайомлення з концепцією доменних подій, що були викладені у розділі теоретичних відомостей, а також у файлі README.md проєкту, який коротко пояснює основне. Реалізація механізмів публікації та обробки подій згідно з предметною областю «Інтернет-магазин побутової хімії» дозволила краще закріпити основні ключові принципи архітектури DDD.