Паттерни взаємодії
Паттерни взаємодії
Вступ
У главах 5–8 розглянуто тактичні патерни проєктування, які визначають різні способи реалізації компонентів системи. Вони демонструють, як моделювати бізнес-логіку та архітектурно організовувати внутрішню частину обмеженого контексту.
У цій главі ми виходимо за межі одного компонента та розглядаємо патерни організації інформаційних потоків між елементами системи.
Перетворення Моделей
Якщо команди, які реалізують два обмежені контексти, вільно спілкуються та готові до співпраці, вони можуть інтегруватися у партнерство. У такому випадку протоколи координації можуть встановлюватися разово, а питання інтеграції вирішуватися шляхом комунікації між командами.
Ще одним методом співпраці є створення спільного ядра (shared kernel), коли команди виділяють та розробляють спільну частину моделі, наприклад, шляхом винесення контрактів до окремого репозиторію.
Взаємодія у відносинах «Клієнт-Постачальник»
У таких відносинах баланс сил зміщується або до постачальника (supplier), або до споживача (consumer).
Перетворення може виконувати одна або обидві сторони:
- Споживач може адаптувати модель постачальника під свої потреби за допомогою захисного шару (anticorruption layer, ACL).
- Постачальник може діяти як сервіс з відкритим протоколом (open-host service, OHS), публікуючи інтеграційно-специфічну мову (integration-specific published language) для зменшення впливу змін на споживачів.
Оскільки логіка перетворення однакова для обох патернів (ACL та OHS), у цій главі розглядаються варіанти реалізації без акценту на їх відмінності, окрім виняткових випадків.
Типи Перетворення Моделей
Перетворення моделі може бути:
- Без збереження стану — відбувається «на льоту» під час обробки вхідних (OHS) або вихідних (ACL) запитів.
- З відстеженням стану — потребує складнішої логіки та бази даних для зберігання змін.
Перетворення без збереження стану
Для реалізації цього типу перетворення в контексті, якому належить трансформація (OHS для постачальника, ACL для споживача), застосовується патерн проксі (proxy).

Реалізація проксі залежить від того, в якому режимі здійснюється взаємодія між обмеженими контекстами, у синхронному чи в асинхронному.
Синхронний режим
Як показано на рис, типовим способом перетворення моделей, використовуваним за синхронного режиму взаємодії, є вбудовування логіки перетворення в кодову базу обмеженого контексту. У сервісі з відкритим протоколом (open-host service) перетворення в публічну мову відбувається під час обробки вхідних запитів, а на рівні запобіжного шару (anticorruption layer) - під час виклику вищого обмеженого контексту.

Цей компонент (АРI-шлюз, API Gateway) може бути програмним рішенням із відкритим вихідним кодом, як, наприклад, Kong або КrakenD або ж керованою службою хмарного провайдера, такою як А WS API Gateway, Google Apigee або Azure API Management.
Для обмежених контекстів, що реалізують патерн сервісу з відкритим протоколом (open-host service), АРI-шлюз (API Gateway), за перетворення внутрішньої моделі на опубліковану мову відповідає мова (рyyyshed language), оптимізована для інтеграції.
Крім того, як показано на рис. 9.3, наявність явного АРI-шлюзу (API Gateway) може полегшити процес управління та супроводу декількох версій API обмеженого контексту.

Запобіжний шар (anticorruption layer), реалізований за допомогою АРIшлюзу, може використовуватися кількома нижчими обмеженими контекстами. У таких випадках, як показано на рис, запобіжний шар (anticorruption layer) діє як обмежений контекст, спеціально призначений для інтеграції.

Асинхронний режим
Для перетворення моделей, використовуваних у режимі асинхронного обміну даними, можна реалізувати проксі: проміжний компонент, що підписується на повідомлення, що надходять із вихідного обмеженого контексту. Проксі-сервер виконає всі необхідні перетворення моделі та перешле отримані повідомлення цільовому передплатнику (рис).

Окрім перетворення моделі повідомлень, проксіруючий компонент також може зменшити шум у цільовому обмеженому контексті, відфільтровуючи непотрібні повідомлення.
Перетворення в асинхронному режимі можна використовувати для перехоплення подій предметної області та перетворення їх в опубліковану мову (рyyshed language) - це забезпечить надійнішу інкапсуляцію деталей реалізації обмеженого контексту.
Крім того, переведення повідомлень на опубліковану мову (рublishед language) дає змогу розрізняти закриті події, призначені для внутрішніх потреб обмеженого контексту, і відкриті події, призначені для інтеграції з іншими обмеженими контекстами.

Перетворення моделей з відстеженням стану
Агрегування вхідних даних
Припустімо, що для оптимізації продуктивності обмежений контекст зацікавлений в агрегуванні вхідних запитів і їхній пакетній обробці. У цьому випадку агрегування може знадобитися для всіх запитів, одержуваних як у синхронному, так і в асинхронному режимі.

Іншим поширеним варіантом агрегування вихідних даних є об'єднання кількох деталізованих повідомлень в одне повідомлення, що містить уніфіковані дані.


У деяких випадках для перетворення з відстеженням стану можна замість створення власного рішення скористатися готовими продуктами, наприклад, платформою потокової обробки (Kafka, А WS Kinesis тощо) або рішенням для пакетної обробки (Apache NiFi, А WS Glue, Spark тощо).
Об'єднання декількох джерел
В обмеженому контексті може знадобитися обробка даних, накопичених відразу з декількох джерел, включно з тими, що надходять з інших обмежених контекстів.
Ще один приклад - обмежений контекст, який повинен обробляти дані з безлічі інших контекстів і реалізовувати складну бізнес-логіку для обробки всіх даних. У цьому випадку може бути корисно розділити складності інтеграції та бізнес-логіки, покриваючи обмежений контекст запобіжним шаром (anticorruption layer), що накопичує дані з усіх інших обмежених контекстів.

Інтеграція агрегатів
У главі 6 вже говорилося, що одним із способів взаємодії між агрегатами і всією іншою системою є публікація подій предметної області. Зовнішні компоненти можуть підписуватися на ці події предметної області і виконувати свою логіку. Але як відбувається публікація подій предметної області в шині повідомлень?
Перш ніж перейти до рішення, давайте розглянемо кілька поширених помилок, які допускаються в процесі публікації подій, і наслідки кожного такого підходу.
Некоректна публікація подій
public class Campaign
{
List<DomainEvent> events;
IMessageBus _messageBus;
public void Deactivate(string reason)
{
foreach (var l in _locations.Values)
{
l.Deactivate();
}
IsActive = false;
var newEvent = new CampaignDeactivated(id, reason);
_events.Add(newEvent);
_messageBus.Publish(newEvent); // Неприйнятно!
}
}
- Подія буде надіслана до того, як новий стан агрегата буде зафіксовано в базі даних.
- Якщо транзакцію бази даних не вдасться зафіксувати, подія вже буде опублікована і передана підписникам.
Спроба публікації на прикладному рівні
Спробуємо інший варіант:
public class ManagementAPI
{
private readonly IMessageBus _messageBus;
private readonly ICampaignRepository _repository;
public ExecutionResult DeactivateCampaign(CampaignId id, string reason)
{
try
{
var campaign = _repository.Load(id);
campaign.Deactivate(reason);
_repository.CommitChanges(campaign);
var events = campaign.GetUnpublishedEvents();
foreach (var e in events)
{
_messageBus.Publish(e); // Все ще ненадійно!
}
campaign.ClearUnpublishedEvents();
}
catch(Exception ex)
{
// Обробка помилок
}
}
}
Патерн вихідних повідомлень (Outbox)
- Атомарна транзакція: Стан оновленого агрегата і нові події предметної області фіксуються в одній і тій самій транзакції.
- Витягування: Ретранслятор повідомлень витягує щойно зафіксовані події з бази даних.
- Публікація: Ретранслятор публікує події в шині повідомлень.
- Позначення: Після успішної публікації ретранслятор позначає події як опубліковані або видаляє їх.

При використанні реляційної бази даних краще скористатися її здатністю атомарно фіксувати дві таблиці та застосувати для зберігання повідомлень спеціально виділену таблицю.

При використанні бази даних NoSQL, яка не підтримує транзакції з декількома документами, вихідні події предметної області повинні бути вбудовані в запис агрегата.
{
"campaign-id": "364b33c3-2171-446d-b652-8e5a7b2belaf",
"state": {
"name": "Autumn 2017",
"publishing-state": "DEACTIVATED",
"ad-locations": []
},
"outbox": {
"campaign-id": "364b33c3-2171-446d-b652-8e5a7b2belaf",
"type": "campaign-deactivated",
"reason": "Goals met",
"published": false
}
}
Отримання неопублікованих подій
Публікуючий ретранслятор (publishing relay) може витягувати нові події предметної області двома способами:
- Pull: запит до постачальника (producer)
Ретранслятор може постійно запитувати базу даних на наявність неопублікованих подій. Необхідно створювати відповідні індекси для мінімізації навантаження. - Push: відстеження журналу транзакцій
Для проактивного виклику можна використовувати журналювання транзакцій (CDC) або потоки подій (наприклад, AWS DynamoDB Streams).
Сага
Один з основних принципів побудови агрегатів — обмеження кожної транзакції одним екземпляром агрегата. Однак бувають ситуації, коли потрібно реалізувати бізнес-процес, що охоплює кілька агрегатів.
Приклад використання саги
При активації рекламної кампанії її рекламні матеріали повинні автоматично відправлятися видавцю. Після отримання підтвердження від видавця статус кампанії змінюється на Published. Якщо видавець відхиляє кампанію, вона позначається як Rejected.
Ця послідовність дій охоплює дві бізнес-сутності: рекламну кампанію та видавництво. Замість об'єднання їх в один агрегат, процес реалізовується у вигляді саги.
Що таке сага?
Сага — це довготривалий бізнес-процес. Йдеться про транзакції та бізнес-процеси, що охоплюють кілька транзакцій.
Сага відстежує події, генеровані відповідними компонентами, і видає подальші команди іншим компонентам. Якщо один з кроків виконується з помилкою, сага відповідає за запуск компенсуючих дій, щоб зберегти узгодженість системи.

Реалізація процесу публікації у сазі
Щоб реалізувати процес публікації, сага повинна відстежувати подію CampaignActivated з агрегату Campaign та події PuЬlishingConfirmed і PuЬlishingRejected з обмеженого контексту AdPuЬlishing.
public class CampaignPuЬlishingSaga
{
private readonly ICampaignRepository _repository;
private readonly IPuЬlishingServiceClient _puЬlishingService;
public void Process(CampaignActivated @event)
{
var campaign = _repository.Load(@event.Campaignid);
var advertisingMaterials = campaign.GenerateAdvertisingMaterials();
_puЬlishingService.SubmitAdvertisement(@event.Campaignid, advertisingMaterials);
}
public void Process(PuЬlishingConfirmed @event)
{
var campaign = _repository.Load(@event.Campaignid);
campaign.TrackPuЬlishingConfirmation(@event.Confirmationid);
_repository.CommitChanges(campaign);
}
public void Process(PuЬlishingRejected @event)
{
var campaign = _repository.Load(@event.Campaignid);
campaign.TrackPuЬlishingRejection(@event.RejectionReason);
_repository.CommitChanges(campaign);
}
}
У складніших ситуаціях сага може вимагати керування станом.
public class CampaignPuЬlishingSaga
{
private readonly ICampaignRepository _repository;
private readonly IList<IDomainEvent> _events;
public void Process(CampaignActivated activated)
{
var campaign = _repository.Load(activated.Campaignid);
var advertisingMaterials = campaign.GenerateAdvertisingMaterials();
var commandissuedEvent = new CommandissuedEvent(
target: Target.PuЬlishingService,
command: new SubmitAdvertisementCommand(activated.Campaignid, advertisingMaterials)
);
_events.Append(activated);
_events.Append(commandissuedEvent);
}
// ... інші методи обробки
}
Відокремлення переходу стану саги від виконання команд гарантує стабільне виконання команд навіть при завершенні процесу помилкою.
Узгодженість
Попри те, що сага керує транзакцією, яка включає зміну кількох компонентів, стан змінених компонентів у підсумку підпорядковується принципу узгодженості (син. підсумкова узгодженість, eventual consistency).
Скористайтеся цією обставиною як керівним принципом, щоб переконатися у відсутності зловживання сагами з метою компенсації невірно обраних меж агрегатів.
Диспетчер процесів
if-else, то це, найімовірніше, диспетчер процесів.Ще одна відмінність диспетчера процесів від саги полягає в тому, що екземпляр саги створюється неявно, а диспетчер процесів повинен бути створений явно як послідовний бізнес-процес.

Приклад: Бронювання поїздки
Бронювання місць для відрядження починається з вибору найекономічнішого маршруту. Це процес, і він має бути реалізований як менеджер процесів.

public class BookingProcessManager
{
private readonly IList<IDomainEvent> events;
// ... поля стану
public void Initialize(Destination destination, TripDefinition parameters, Employeeid traveler)
{
this.destination = destination;
_parameters = parameters;
this.traveler = traveler;
route = _routing.Calculate(destination, parameters);
var routeGenerated = new RouteGeneratedEvent(Bookingid: _id, Route: route);
var commandIssuedEvent = new CommandIssuedEvent(
command: new RequestEmployeeApproval(traveler, route)
);
_events.Append(routeGenerated);
_events.Append(commandIssuedEvent);
}
public void Process(RouteConfirmed confirmed)
{
var commandIssuedEvent = new CommandIssuedEvent(
command: new BookFlights(route, _parameters)
);
_events.Append(confirmed);
_events.Append(commandIssuedEvent);
}
// ... обробка інших подій
}
У цьому прикладі диспетчер процесів має свій ідентифікатор і постійний стан. Він підписується на події та створює команди, які будуть оброблятися ретранслятором вихідних повідомлень.
Висновок
::field-group
items:
- title: Перетворення Моделей description: Може бути синхронним або асинхронним, зі збереженням стану або без нього. Використовується для ACL або OHS.
- title: Патерн Outbox description: Гарантує надійну публікацію подій предметної області шляхом атомарного коміту зі станом агрегата.
- title: Сага description: Координує прості лінійні бізнес-процеси, що охоплюють кілька компонентів, використовуючи компенсацію.
- title: Диспетчер процесів description: Централізовано керує складними бізнес-процесами зі складними розгалуженнями логіки та явним станом.
::
Опанування цих патернів дозволяє будувати надійні, масштабовані системи з чітко розділеними межами та впорядкованими потоками даних.
Завдання
- Який із патернів інтеграції з обмеженим контекстом вимагає реалізації логіки перетворення моделі?
- А) Конформіст (conformist).
- Б) Захисний шар (anticorruption layer).
- В) Сервіс з відкритим протоколом (open-host service).
- Г) Б і В.
- Яка мета патерна вихідних повідомлень (outbox pattern)?
- А) Відокремити інфраструктуру обміну повідомленнями від рівня бізнес-логіки системи.
- Б) Забезпечити надійну публікацію повідомлень.
- В) Підтримати реалізацію патерна моделі предметної області, заснованого на подіях (event-sourced domain model).
- Г) А і Б.
- Які є інші можливі варіанти використання патерна вихідних повідомлень (outbox pattern), окрім публікації повідомлень на шині повідомлень?
- Чим сага відрізняється від диспетчера процесів?
- А) Диспетчер процесів вимагає явного створення екземпляра, а екземпляр саги створюється неявно.
- Б) На відміну від диспетчера процесів, сага ніколи не потребує збереження стану свого виконання.
- В) Сазі потрібні компоненти для реалізації патерна Event Sourcing, а диспетчеру процесів — ні.
- Г) Диспетчер процесів більше підходить для складних бізнес-процесів.
- Д) Правильними є твердження А і Г.