Software Engineering

Паттерни взаємодії

Паттерни взаємодії

Вступ

У главах 5–8 розглянуто тактичні патерни проєктування, які визначають різні способи реалізації компонентів системи. Вони демонструють, як моделювати бізнес-логіку та архітектурно організовувати внутрішню частину обмеженого контексту.

У цій главі ми виходимо за межі одного компонента та розглядаємо патерни організації інформаційних потоків між елементами системи.

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

Перетворення Моделей

Обмежений контекст визначає межі моделі та єдиної мови (ubiquitous language). У главі 3 було розглянуто різні патерни проєктування взаємодії між обмеженими контекстами.

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

Ще одним методом співпраці є створення спільного ядра (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).

Цей компонент (АР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) діє як обмежений контекст, спеціально призначений для інтеграції.

Такі обмежені контексти, що відповідають переважно за перетворення моделей для зручнішого використання іншими компонентами, часто називають контекстами обміну.


Асинхронний режим

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

Окрім перетворення моделі повідомлень, проксіруючий компонент також може зменшити шум у цільовому обмеженому контексті, відфільтровуючи непотрібні повідомлення.

Перетворення моделі в асинхронному режимі необхідне під час реалізації служби з відкритим хостом. Вельми поширеною помилкою є розробка та надання опублікованої мови (рyyshead language) для об'єктів моделі і дозвіл публікації подій предметної області в їхньому вихідному вигляді, що розкриває модель реалізації обмеженого контексту.

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

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


Перетворення моделей з відстеженням стану

Для більш істотних перетворень моделей, наприклад, коли механізм перетворення має агрегувати вихідні дані або об'єднати дані з кількох джерел у єдину модель, - може знадобитися перетворення з відстеженням стану.

Агрегування вхідних даних

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

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

Перетворення моделі з накопиченням вхідних даних не може бути реалізовано за допомогою АРI-шлюзу (API Gateway), тому тут потрібне складніше обробка з відстеженням стану. Для відстеження вхідних даних і відповідного їх опрацювання логіці перетворення потрібне власне постійне сховище.

У деяких випадках для перетворення з відстеженням стану можна замість створення власного рішення скористатися готовими продуктами, наприклад, платформою потокової обробки (Kafka, А WS Kinesis тощо) або рішенням для пакетної обробки (Apache NiFi, А WS Glue, Spark тощо).

Об'єднання декількох джерел

В обмеженому контексті може знадобитися обробка даних, накопичених відразу з декількох джерел, включно з тими, що надходять з інших обмежених контекстів.

Типовим прикладом такої обробки є застосування патерну backend-for-frontend, в якому користувацький інтерфейс має об'єднувати дані, що надходять одразу від кількох сервісів.

Ще один приклад - обмежений контекст, який повинен обробляти дані з безлічі інших контекстів і реалізовувати складну бізнес-логіку для обробки всіх даних. У цьому випадку може бути корисно розділити складності інтеграції та бізнес-логіки, покриваючи обмежений контекст запобіжним шаром (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); // Неприйнятно!
    }
}
Публікація події предметної області прямо з агрегата є поганою практикою з двох причин:
  1. Подія буде надіслана до того, як новий стан агрегата буде зафіксовано в базі даних.
  2. Якщо транзакцію бази даних не вдасться зафіксувати, подія вже буде опублікована і передана підписникам.

Спроба публікації на прикладному рівні

Спробуємо інший варіант:

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)

Патерн вихідних повідомлень (Outbox) забезпечує надійну публікацію подій предметної області, гарантуючи, що стан агрегата та події зафіксовані атомарно.
  1. Атомарна транзакція: Стан оновленого агрегата і нові події предметної області фіксуються в одній і тій самій транзакції.
  2. Витягування: Ретранслятор повідомлень витягує щойно зафіксовані події з бази даних.
  3. Публікація: Ретранслятор публікує події в шині повідомлень.
  4. Позначення: Після успішної публікації ретранслятор позначає події як опубліковані або видаляє їх.

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

При використанні бази даних 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) може витягувати нові події предметної області двома способами:

  1. Pull: запит до постачальника (producer)
    Ретранслятор може постійно запитувати базу даних на наявність неопублікованих подій. Необхідно створювати відповідні індекси для мінімізації навантаження.
  2. Push: відстеження журналу транзакцій
    Для проактивного виклику можна використовувати журналювання транзакцій (CDC) або потоки подій (наприклад, AWS DynamoDB Streams).
Патерн вихідних повідомлень (outbox pattern) гарантує як мінімум одноразову (at least once) доставку повідомлень.

Сага

Один з основних принципів побудови агрегатів — обмеження кожної транзакції одним екземпляром агрегата. Однак бувають ситуації, коли потрібно реалізувати бізнес-процес, що охоплює кілька агрегатів.

Приклад використання саги

При активації рекламної кампанії її рекламні матеріали повинні автоматично відправлятися видавцю. Після отримання підтвердження від видавця статус кампанії змінюється на 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: Централізовано керує складними бізнес-процесами зі складними розгалуженнями логіки та явним станом.

::

Опанування цих патернів дозволяє будувати надійні, масштабовані системи з чітко розділеними межами та впорядкованими потоками даних.


Завдання

  1. Який із патернів інтеграції з обмеженим контекстом вимагає реалізації логіки перетворення моделі?
    • А) Конформіст (conformist).
    • Б) Захисний шар (anticorruption layer).
    • В) Сервіс з відкритим протоколом (open-host service).
    • Г) Б і В.
  1. Яка мета патерна вихідних повідомлень (outbox pattern)?
    • А) Відокремити інфраструктуру обміну повідомленнями від рівня бізнес-логіки системи.
    • Б) Забезпечити надійну публікацію повідомлень.
    • В) Підтримати реалізацію патерна моделі предметної області, заснованого на подіях (event-sourced domain model).
    • Г) А і Б.
  1. Які є інші можливі варіанти використання патерна вихідних повідомлень (outbox pattern), окрім публікації повідомлень на шині повідомлень?
  1. Чим сага відрізняється від диспетчера процесів?
  • А) Диспетчер процесів вимагає явного створення екземпляра, а екземпляр саги створюється неявно.
  • Б) На відміну від диспетчера процесів, сага ніколи не потребує збереження стану свого виконання.
  • В) Сазі потрібні компоненти для реалізації патерна Event Sourcing, а диспетчеру процесів — ні.
  • Г) Диспетчер процесів більше підходить для складних бізнес-процесів.
  • Д) Правильними є твердження А і Г.