Software Engineering

6. Архітектурні патерни

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

6. Архітектурні патерни

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

Співставлення бізнес-логіки та архітектурних патернів

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

  • Взаємодію з користувачами для отримання вхідних даних.
  • Надання вихідних даних.
  • Використання механізмів зберігання для фіксації стану.
  • Інтеграцію із зовнішніми системами та постачальниками інформації. ::

Відсутність чіткої організації у вирішенні цих завдань ускладнює внесення змін до коду. При необхідності змін у бізнес-логіці складно одразу визначити, які частини коду потрібно модифікувати. Деякі зміни можуть мати несподіваний вплив на зовсім, здавалося б, не пов'язані частини системи. Так само легко пропустити ті фрагменти коду, які необхідно змінити. Усі ці проблеми значно збільшують витрати на підтримку кодової бази.

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

Архітектурні патерни впливають на:

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

Три основні архітектурні патерни

Давайте розглянемо три ключові архітектурні патерни додатків:

  1. Шарова архітектура (Layered Architecture).
  2. Порти й адаптери (Ports and Adapters).
  3. CQRS (Command Query Responsibility Segregation).

Шарова архітектура

Одним із найпоширеніших архітектурних патернів є шарова архітектура (Layered Architecture). У цьому підході кодова база структурується в горизонтальні шари, кожен з яких вирішує одну з таких технічних задач:
  • Взаємодія з користувачами.
  • Реалізація бізнес-логіки.
  • Збереження даних.

Структура шарів представлена на рисунку:

  • Шар представлення (Presentation Layer).
  • Шар бізнес-логіки (Business Logic Layer).
  • Шар доступу до даних (Data Access Layer).


Шар представлення (Presentation Layer)

На шарі представлення реалізується користувацький інтерфейс програми для взаємодії з її користувачами. У традиційній формі цей шар включає графічний інтерфейс, наприклад, веб-інтерфейс або настільний застосунок.

У сучасних системах шар представлення охоплює ширший спектр засобів взаємодії:

  • Графічний інтерфейс користувача (GUI).
  • Інтерфейс командного рядка (CLI).
  • API для інтеграції з іншими системами.
  • Підписка на події у брокері повідомлень.
  • Теми повідомлень для публікації подій.
Шар представлення забезпечує отримання системою запитів із зовнішнього середовища та передачу результатів. По суті, це публічний інтерфейс програми.

Шар бізнес-логіки (Business logic layer)

Як видно з назви, цей шар відповідає за реалізацю та інкапсуляцію бізнес-логіки програми. Саме тут реалізуються бізнес-рішення. Як зазначає Ерік Еванс (Eric Evans), цей шар є серцем програмного забезпечення.

На цьому шарі реалізуються патерни бізнес-логіки, описані в розділах 5-7, наприклад, активні записи або модель предметної області (див. рис.).


Шар доступу до даних (Data access layer)

Шар доступу до даних забезпечує доступ до механізмів зберігання інформації.

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

  1. Багато баз даних. З часів революції NoSQL система зазвичай працює з кількома базами даних. Наприклад, документоорієнтоване сховище може виступати в ролі робочої бази даних, пошуковий індекс — для динамічних запитів, а база даних в оперативній пам'яті — для високопродуктивних операцій.
  2. Інші засоби зберігання. Традиційні бази даних не є єдиним засобом зберігання інформації. Наприклад, для зберігання файлів може використовуватися хмарне об'єктне сховище, а для зв'язку між функціями програми — шина повідомлень.
  3. Зовнішні постачальники інформації. Цей шар також включає інтеграцію з API зовнішніх систем або сервісами хмарних провайдерів, такими як переклад, дані фондового ринку, розпізнавання аудіо тощо (див. рис)


Зв'язок між шарами

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

На рис. 8.5 шар представлення звертається лише до шару бізнес-логіки. Він не знає нічого про дизайн шару доступу до даних.


Варіація: Сервісний шар (Service layer)

Зазвичай патерн шарової архітектури розширюється додатковим сервісним шаром.

Сервісний шар (Service layer)

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

Сервісний шар виступає посередником між існуючими шарами програми: представлення та бізнес-логіки.

Розглянемо наступний код:

namespace MvcApplication.Controllers
public class UserController: Controller
{
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(ContactDetails contactDetails)
    {
        OperationResult result = null;
        try
        {
            _db.StartTransaction();
            var user = new User();
            user.SetContactDetails(contactDetails);
            user.Save();
            _db.Commit();
            result = OperationResult.Success;
        }
        catch (Exception ex)
        {
            _db.Rollback();
            result = OperationResult.Exception(ex);
        }
        return View(result);
    }
}

Показаний у прикладі MVC-контролер належить до шару представлення. Він надає інтерфейс для створення нового користувача. Створення нового екземпляра та його збереження здійснюється через об'єкт активного запису user.

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

Сервісний шар як логічна межа

Важливо зазначити, що в контексті архітектурного патерна сервісний шар є логічною межею. Його не слід розглядати як фізичний сервіс.

Сервісний шар виступає фасадом шару бізнес-логіки: він надає інтерфейс, який відповідає методам публічного інтерфейсу, інкапсулюючи виклики нижчих шарів.

Наприклад:

interface CampaignManagementService {
    OperationResult CreateCampaign(CampaignDetails details);
    OperationResult Publish(CampaignId id, PublishingSchedule schedule);
    OperationResult Deactivate(CampaignId id);
    OperationResult AddDisplayLocation(CampaignId id, DisplayLocation newLocation);
}

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


Рефакторинг з винесенням логіки оркестрації у сервісний шар

namespace ServiceLayer {
    public class UserService {
        public OperationResult Create(ContactDetails contactDetails) {
            OperationResult result = null;
            try {
                _db.StartTransaction();
                var user = new User();
                user.SetContactDetails(contactDetails);
                user.Save();
                _db.Commit();
                result = OperationResult.Success;
            } catch (Exception ex) {
                _db.Rollback();
                result = OperationResult.Exception(ex);
            }
            return result;
        }
    }
}

Переваги явного сервісного шару

1. Багаторазове використання: Один і той самий сервісний шар може бути використаний кількома публічними інтерфейсами, наприклад, графічним інтерфейсом користувача та API. Це усуває необхідність дублювати логіку оркестрації.2. Модульність коду: Усі пов'язані методи зосереджені в одному місці.3. Розділення логіки: Чітке розділення шару представлення та бізнес-логіки.4. Спрощення тестування: Функціональність бізнес-логіки стає легше тестувати.

Коли сервісний шар не потрібен?

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

Коли сервісний шар необхідний?

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

Термінологія

В інших джерелах інформації можуть зустрічатися й інші терміни, які використовуються для опису шарової архітектури:
  • Шар представлення = шар користувацького інтерфейсу.
  • Сервісний шар = прикладний шар.
  • Шар бізнес-логіки = шар предметної області = шар моделі.
  • Шар доступу до даних = шар інфраструктури.

Щоб уникнути плутанини, патерн тут представлено з використанням початкової термінології. Водночас я схиляюся до таких понять, як «шар користувацького інтерфейсу» та «шар інфраструктури», оскільки ці терміни краще відображають обов’язки сучасних систем та шарів додатків, не допускаючи плутанини з фізичними межами сервісів.


Коли краще використовувати шарову архітектуру

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

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

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


Додатково: порівняння шарів і рівнів

Шарову архітектуру часто плутають із архітектурою N-Tier (багаторівневою) та навпаки. Незважаючи на схожість між двома патернами, шари й рівні (tiers) концептуально відрізняються:
  • Шар — це логічна межа.
  • Рівень — це фізична межа.

Усі шари в шаровій архітектурі мають один і той самий життєвий цикл: вони реалізуються, розвиваються й розгортаються як єдине ціле. А рівень — це незалежно розгортований сервіс, сервер або система.

Розглянемо, наприклад, систему з архітектурою N-Tier:

  • Браузер, який працює на настільному комп’ютері або мобільному пристрої.
  • Зворотний проксі-сервер, який перенаправляє запити до веб-додатку.
  • Веб-додаток, який працює на веб-сервері й взаємодіє із сервером бази даних.

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

Шари всередині веб-додатку є логічними межами.


Порти й адаптери (Ports and Adapters)

Архітектура портів і адаптерів усуває недоліки шарової архітектури й краще підходить для реалізації складнішої бізнес-логіки.

Цікаво, що обидва патерни дуже схожі. Рефакторимо шарову архітектуру в архітектуру портів і адаптерів.

Термінологія

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


Принцип інверсії залежностей (Dependency Inversion Principle)

Принцип інверсії залежностей (Dependency Inversion Principle — DIP) говорить, що високорівневі модулі, які реалізують бізнес-логіку, не повинні залежати від низькорівневих модулів. Але в традиційній шаровій архітектурі відбувається саме це. Шар бізнес-логіки залежить від шару інфраструктури.

Щоб відповідати принципу DIP, давайте, як показано на рис, розвернемо залежності у зворотному напрямку.

Шар бізнес-логіки

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

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

Архітектура, зображена на рис, представляє архітектурний патерн портів і адаптерів (Ports and Adapters). Шар бізнес-логіки не залежить ні від одного з нижніх шарів, що дозволяє реалізувати патерни моделі предметної області та моделі предметної області, заснованої на подіях (Event Sourced Domain Model).


Чому цей патерн називається порти й адаптери?

Щоб відповісти на це питання, розглянемо, як інфраструктурні компоненти інтегровані з бізнес-логікою.

Інтеграція інфраструктурних компонентів

Основна мета архітектури портів і адаптерів — відокремити бізнес-логіку системи від її інфраструктурних компонентів.

Замість того, щоб посилатися на інфраструктурні компоненти або викликати їх напряму, шар бізнес-логіки визначає «порти», які мають бути реалізовані на шарі інфраструктури. А на шарі інфраструктури реалізуються «адаптери» — конкретні реалізації інтерфейсів портів для роботи з різними технологіями (див. рис).

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

Наприклад, ось як виглядає можливе визначення порту та конкретний адаптер для шини повідомлень:

namespace App.BusinessLogicLayer
{
    public interface IMessaging
    {
        void Publish(Message payload);
        void Subscribe(Message type, Action callback);
    }
}

namespace App.Infrastructure.Adapters
{
    public class SQSBus: IMessaging {
        // ... implementation details
    }
}

Варіанти

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

І все ж ці патерни можна помилково вважати концептуально різними. Це ще один приклад важливості єдиної мови.

Коли доцільніше використовувати порти й адаптери

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

Розділення відповідальності команд і запитів

(Command-Query Responsibility Segregation)

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

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

Мультипарадигмальне моделювання

(Polyglot modelling)

Використання єдиної моделі для всіх потреб системи може бути ускладненим або навіть неможливим. Наприклад, як уже згадувалося у главі 7, обробка транзакцій (online transaction processing – OLTP) та аналітична обробка (online analytical processing – OLAP) можуть вимагати різних представлень даних.

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

І, нарешті, що потрібно згадати – патерн CQRS тісно пов’язаний із джерелом подій (event sourcing). Спочатку CQRS був визначений для подолання обмежень у виконанні запитів при використанні моделі, заснованої на подіях (event sourced domain model): за один раз можна було запитувати події лише одного екземпляра агрегату.

Застосування патерна CQRS дозволяє використовувати одразу кілька механізмів збереження для представлення різних моделей даних розроблюваної системи.

Реалізація

Як випливає із назви, патерн розділяє обов’язки моделей системи. Існують два типи моделей: модель виконання команд і моделі читання.

Модель виконання команд

CQRS виділяє єдину модель для виконання операцій, які змінюють стан системи (системні команди). Ця модель використовується для реалізації бізнес-логіки, перевірки дотримання правил і забезпечення інваріантів.

Модель виконання команд також є єдиною моделлю, що представляє строго узгоджені дані – джерелом істини (source of truth).

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

Моделі читання (проєкції)

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

Модель читання – це попередньо кешована проєкція. Вона може знаходитися у базі даних, у звичайному файлі або у кеші в пам’яті. Правильна реалізація CQRS дозволяє стерти всі дані проєкції і відновити її з нуля.

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

Проєктування моделей читання

Щоб моделі читання запрацювали, система повинна проєктувати зміни із моделі виконання команд.

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

А тепер давайте розглянемо два способи створення проєкцій: синхронний та асинхронний.

Синхронні проєкції

При синхронному оновленні проєкцій оновлення даних відбувається за моделлю наздоганяючої підписки (catch-up subscription):

  1. Механізм проєктування запитує в OLTP-базі нові або оновлені записи після останньої контрольної точки (checkpoint).
  2. Механізм проєктування використовує оновлені дані для створення або оновлення моделей читання.
  3. Механізм проєктування зберігає контрольну точку останнього обробленого запису.

Цей процес показаний на рис. у вигляді діаграми послідовності.

Щоб наздоганяюча підписка працювала, модель виконання команд повинна встановлювати контрольну точку (checkpoint) для всіх нових або оновлених записів бази даних. Механізм зберігання також має підтримувати запит записів на основі контрольних точок.

Контрольна точка може бути реалізована за допомогою можливостей баз даних. Наприклад, як показано на таблиці. , для генерації унікальних інкрементальних чисел під час вставки або оновлення рядка у SQL Server може використовуватися стовпець «rowversion».

IdІм'яПрізвищеCheckpoint
1ТомКук0x0000000000001792
2ГарольдЕліот0x0000000000001793
3ДіанаДеніелс0x0000000000001796
4ДіанаДеніелс0x0000000000001795
Метод синхронного проєктування спрощує додавання нових проєкцій і регенерацію існуючих із самого початку. Достатньо лише скинути контрольну точку на 0.

Асинхронні проєкції

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

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

Тому завжди рекомендується реалізовувати синхронну проєкцію, а в разі необхідності надбудовувати над нею додаткову асинхронну проєкцію.


Розподіл моделей

В архітектурі CQRS відповідальності моделей розділені відповідно до їх типу. Команда може працювати лише в строго узгодженій моделі виконання команд. Запит не може безпосередньо змінювати збережений стан системи – ні моделі читання, ні моделі виконання команд.

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

Коли краще використовувати CQRS

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

З точки зору інфраструктури, CQRS дозволяє використовувати можливості різних типів баз даних: наприклад, для збереження моделі виконання команд використовувати реляційні бази даних, для повнотекстового пошуку – індекс пошуку, а для швидкого витягування даних – заздалегідь оброблені файли.

Крім того, CQRS природно підходить для моделей предметної області, заснованих на подіях (event sourced domain model).


Сфера застосування

Розглянуті тут патерни – багаторівнева архітектура, архітектура портів і адаптерів та CQRS – не повинні слугувати загальними організаційними принципами для всієї системи.

Наприклад, в обмеженому контексті, який охоплює кілька піддоменів, піддомени можуть бути різних типів: основні (core), допоміжні (supporting) чи універсальні (generic).

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

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


Висновок

::field-group

items:

  • title: Багатошарова архітектура description: Передбачає поділ кодової бази на основі технологічних завдань. Добре підходить для систем, які використовують активні записи.
  • title: Архітектура портів і адаптерів description: Бізнес-логіка стає центральною і відділяється від усіх інфраструктурних залежностей. Ідеально для моделі предметної області.
  • title: Патерн CQRS description: Представляє одні й ті самі дані одразу в декількох моделях. Обов’язковий для event-sourced систем, але корисний усюди, де потрібні різні проєкції.

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


Вправи

  1. Які з розглянутих архітектурних патернів можна використовувати з бізнес-логікою, реалізованою у вигляді патерна активного запису?
    - А) Багатошарова архітектура.
    - Б) Порти і адаптери.
    - В) CQRS.
    - Г) А і Б.
    
  1. Який із розглянутих архітектурних патернів відокремлює бізнес-логіку від інфраструктурних задач?
    • А) Багатошарова архітектура.
    • Б) Порти і адаптери.
    • В) CQRS.
    • Г) Б і В.
  1. Припустимо, що при реалізації патерна портів і адаптерів необхідно інтегрувати шину повідомлень хмарного провайдера. На якому шарі повинна бути реалізована інтеграція?
    • А) На шарі бізнес-логіки.
    • Б) На прикладному шарі.
    • В) На шарі інфраструктури.
    • Г) На будь-якому шарі.
  1. Яке з наведених тверджень стосовно патерна CQRS є правильним?
    • А) Асинхронні проекції легше масштабувати.
    • Б) Можна використовувати або синхронну, або асинхронну проекцію, але не обидві одночасно.
    • В) Команда не повертає ніякої інформації стороні, що викликає. Для отримання результатів виконаних дій код, що викликає, завжди повинен використовувати моделі читання.
    • Г) Команда може повертати інформацію, якщо вона виходить зі строго узгодженої моделі.
    • Д) А і Г.
  1. Патерн CQRS дозволяє представляти одні й ті самі бізнес-об’єкти у декількох моделях збереження інформації, дозволяючи таким чином в одному й тому ж обмеженому контексті працювати з декількома моделями. Чи суперечить це уявленню про те, що обмежений контекст є межею моделі?