• База знань
  • /
  • Блог
  • /
  • Wiki
  • /
  • ONLINE CHAT
+380 (44) 364 05 71

Стаття також доступна російською (перейти до перегляду).

DDD, Hexagonal, Onion, Clean, CQRS

Вступ

Архітектура MVC та інша «класика» не задовольняє в повній мірі запитам сучасної веб-розробки. І тому пошук досконалої програмної архітектури веб-додатків є на сьогодні вельми актуальною темою серед розробників. Представляємо довільний переклад роботи Herberto Graça, присвяченій вказаній темі. Автор пропонує свій варіант архітектури під умовною назвою Explicit architecture, котра включає в себе лише найкраще з того, що було раніше представлено провідними фахівцями у цій галузі.

Базові архітектури додатків

Концепція Explicit architecture не виникла на голому місці, а явилася результатом комбінування елементів архітектур, вже перевірених часом. І тому буде цілком доцільним хоча б у стислому вигляді представити характеристики цих базових архітектур.

Архітектура EBI

Концепція EBI (Entity Boundary Interactor) вперше була описана на початку 90-х років І. Джейкобсоном у своїй книзі, присвяченій об’єктно-орієнтованій розробці програмного забезпечення. Але тоді він використовував трохи іншу абревіатуру для позначення технології (Entity Interface Control) із міркувань уникнення плутанини із існуючими концепціями. Але принцип залишається тим самим.

Сутність концепції полягає в чіткому розділенні архітектури додатку на три умовно незалежні блоки:

  • Дані предметної області та набір операцій над ними (Entity);
  • Набір інструментів, котрі залежать від системного середовища та забезпечують зв’язок системи із зовнішнім середовищем (Interface);
  • Набір управляючих операцій, котрі не увійшли до інших блоків (Control).

На Малюнку 1. наведено графічну інтерпретацію вказаних блоків, котра дає уявлення про сутність кожного з них.

Малюнок 1. Графічна інтерпретація блоків EBI.

Малюнок 1. Графічна інтерпретація блоків EBI.

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

Архітектура Ports & Adapters

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

На Малюнку 2. графічно відображена сутність вказаної концепції.

Інтерпретація концепції Ports & Adapters.

Малюнок 2. Інтерпретація концепції Ports & Adapters.

Згідно цієї концепції, архітектура додатку може бути представлена у вигляді трьох рівнів: представлення, бізнес-логіка та дані (Мал. 3).

Рівні архітектури Ports & Adapters

Малюнок 3. Рівні архітектури Ports & Adapters.

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

Технологія DDD

Концепція DDD (Domain-Driven Design) вперше була запропонована Е. Евансом у своїй книзі, присвяченій проектуванню додатків на базі знань про предметну область для котрої розробляється додаток. Її основне завдання – створення узагальненої концепції розуміння принципів побудови та роботи додатку як зі сторони розробників, так і замовників. Тобто, концепція передбачає максимальну орієнтованість розробки на предметну область додатку.

Згідно DDD архітектура додатку має бути розділена на кілька незалежних рівнів:

  • Користувацький інтерфейс (UI);
  • Прикладний рівень (Application);
  • Рівень домену (Domain);
  • Інфраструктура (Infrastructure).

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

Архітектура Onion

Концепція була запропонована розробником Д. Палермо у 2008 році, котра за своєю суттю максимально наближена до архітектури Ports & Adapters, котру ми розглянули раніше. Однак на відміну від останньої, до неї додається ще один рівень абстракції – розбиття системи на внутрішній та зовнішній рівні. Внутрішній рівень включає бізнес-логіку додатку, а зовнішній – механізми доставки та інфраструктуру (див. Мал. 4).

 Архітектура Onion

Малюнок 4. Архітектура Onion.

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

  • Внутрішні рівні не знають про будову зовнішніх;
  • Зовнішні залежать від внутрішніх.

Це означає, що як і в Ports & Adapters напрямок зв’язку, тобто залежності направлені з зовні до центру системи, що є характерною ознакою будь-якої незалежної об’єктної моделі, у нашому випадку – моделі предметної області.

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

Основні компоненти архітектури Explicit

Наша концепція побудови архітектури додатку спирається на ті ж самі принципи, котрі були використані авторами технологій EBI та Ports & Adapters, а саме:

  • Від’єднання зовнішнього коду додатку від внутрішнього;
  • Існування окремих незалежних адаптерів для з’єднання ядра із зовнішнім світом.

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

Виділимо три основні блоки архітектури (див. Мал. 5):

  • Інструменти запуску та управління користувацьким інтерфейсом (User Interface);
  • Ядро додатку, котре включає системну бізнес-логіку (Application Core);
  • Інфраструктура, котра містить набір інструментів для з’єднання ядра із зовнішніми постачальниками даних (Infrastructure).

Основні блоки архітектури Explicit

Малюнок 5. Основні блоки архітектури Explicit.

Ядро Application Core є основним блоком системи, котре може взаємодіяти із багатьма типами зовнішніх постачальників команд та даних – мобільні та «стаціонарні» веб-додатки, консолі терміналу та будь-які інші програмні засоби, котрі використовують API.

Як і в багатьох базових архітектурах, напрямок виконання команд тут йде від зовнішнього інтерфейсу до ядра додатку. Потім від ядра до блоку Infrastructure, котрий зазвичай включає різні типи сховищ даних. Результати виконання запиту з Infrastructure передаються до блоку Application Core, а звідти вже передаються до зовнішнього інтерфейсу, з котрого було здійснено запит. Графічно цей процес представлений на Мал. 6.

Схема взаємодії основних компонентів архітектури Explicit.

Малюнок 6. Схема взаємодії основних компонентів архітектури Explicit.

Інструменти, котрі використовує додаток

Будь-який веб-додаток при своїй роботі використовує ряд інструментів (Instruments), котрі не відносяться до коду його ядра. Ці елементи можуть знаходитися як на стороні UI (перша група), так і на стороні протилежної частини архітектури – у межах блоку Infrastructure (друга група).

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

Друга група інструментів, навпаки, орієнтована на виконання команд ядра додатку. До неї зазвичай відносяться СУБД, пошукові сервіси та інший подібний інструментарій.

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

Порти

Елемент Port є точкою входу до блоку системи Application Core та визначає яким чином останній буде взаємодіяти із елементами групи Instruments. У грубому наближенні цей елемент можна вважати звичайним інтерфейсом, котрий забезпечує зв’язок ядра із зовнішніми постачальниками даних. Вони є складовою частиною бізнес-логіки і тому створюються лише для потреб блоку Application Core.

Групи адаптерів

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

Так само, як і елементи Instruments, адаптери розділяються на дві групи – первинні та вторинні. Перші «працюють» із інструментами першої групи (керуючими), другі – із інструментами другої групи (залежними від ядра).

Первинні адаптери

Ці елементи за допомогою портів передають дані та управляючі команди на вхід Application Core, попередньо перетворюючи їх у виклики методів (див. Мал. 7).

Схема взаємодії первинних адаптерів із ядром додатку

Малюнок 7. Схема взаємодії первинних адаптерів із ядром додатку.

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

Вторинні адаптери

Ці елементи реалізують визначений інтерфейс та вводяться у Application Core у тому місці, де вимагається наявність елементу Port (див. Мал. 8).

Схема взаємодії вториинних адаптерів із ядром додатку

Малюнок 8. Схема взаємодії вториинних адаптерів із ядром додатку.

На практиці це виглядає приблизно так. Трапляється ситуація, коли для роботи додатку необхідно змінити тип сховища даних, наприклад, з MySQL на MongoDB. У такому разі ми створюємо новий елемент Adapter, котрий і реалізує інтерфейс для MongoDB. Після цього залишається лише замінити ним попередній елемент і система готова до роботи із новим типом сховища.

Інверсія керування додатком

Як і більшість базових архітектур додатків, котрі ми розглядали вище, наша система побудована таким чином, що залежності направлені ззовні до центру, а не навпаки. Тобто, наявна інверсія керування на системному рівні. Це проявляється, зокрема, в тому, що ядро додатку залежить лише від елементів Port, в той час як адаптери залежать не лише від конкретного елементу Port, але і від відповідного елементу групи Instrumets (див. Мал. 9).

Інверсія керування додатком

Малюнок 9. Інверсія керування додатком.

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

Будова блоку Application Core

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

Прикладний рівень

Цей рівень, зокрема, включає:

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

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

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

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

Під варіантами застосування тут розуміються процеси, котрі можуть бути запущені в центральному компоненті системи із різними типами інтерфейсів. Наприклад, для звичайних користувачів може використовуватися один тип інтерфейсу, для Менеджерів – другий, для терміналів – третій і т. д. Вказані процеси можуть ініціювати на цьому рівні підключення того, або іншого інтерфейсу (див. Мал. 10).

Схема взаємодії елементів системи на прикладному рівні

Рівень домену

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

Рівень домену

Малюнок 11. Рівень домену.

Сервіси

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

Ми пропонуємо включити набір сутностей та операцій над ними у окрему службу. Ця служба буде належати до рівня домену і тому буде ізольована від прикладного рівня. Але водночас вона зможе використовувати інші служби та об’єкти моделі домену.

Модель домену

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

Блоки функцій або компоненти

Традиційний підхід, що до диференціації коду додатку, полягає у розбитті його на окремі шари або рівні (див. Мал. 12).

Диференціація коду по рівням

Малюнок 12. Диференціація коду по рівням.

Однак, у цьому випадку архітектура додатку слабо пов’язана із самим додатком та особливостями предметної області, а лише із особливостями Фреймворку, за допомогою котрого він був створений. І тому ми пропонуємо використання підходу, коли код розбивається не на рівні, а на блоки функцій. Ця ідея вже була висловлена раніше, зокрема, розробником Р. Мартіном (див. Мал. 13).

Диференціація коду додатку по компонентам.

Малюнок 13. Диференціація коду додатку по компонентам.

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

Приклад розбиття коду додатку на компоненти

Малюнок 14. Приклад розбиття коду додатку на компоненти.

Роз’єднання компонентів

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

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

Будова логіки

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

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

Використання даних

Модель обігу даних між компонентами передбачає існування двох способів їх використання:

  • Загальне сховище;
  • Окреме сховище для кожного компоненту.

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

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

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

Управління системою

Як вже зазначалося вище, напрямок взаємодії основних блоків системи є наступним:

UI > Application Core > Infrastructure > Application Core > UI. Але ми ще не з’ясували ступінь залежності між блоками, тобто, «хто є головним» і як здійснюється управління. Для пояснення цього скористаємося UML-схемами, наведеними нижче.

Для випадку відсутності управляючої шини контроллери можуть залежати від об’єктів Query або служби веб-додатку (див. Мал. 15).

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

Малюнок 15. Організація системи управління Explicit у випадку відсутності шини управління.

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

Об’єкт Query має у своєму складі запит для повернення необроблених даних у DTO. Ці дані згодом будуть представлені користувачу системи.

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

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

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

Малюнок 16. Організація системи управління Explicit у випадку наявності шини управління.

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

Окрім того, нами витримане головне правило для будь-якої сучасної програмної архітектури – напрямок залежностей йде ззовні всередину, але не навпаки (див. Мал. 17).

Діаграма залежностей для архітектури Explicit.

Малюнок 17. Діаграма залежностей для архітектури Explicit.

Підписуйтесь на наш телеграм-канал https://t.me/freehostua, щоб бути в курсі нових корисних матеріалів.

Дивіться наш канал Youtube на https://www.youtube.com/freehostua.

Ми у чомусь помилилися, чи щось пропустили?

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

Дата: 03.04.2024
Автор: Олександр Ровник
Голосування

Авторам статті важлива Ваша думка. Будемо раді його обговорити з Вами:

comments powered by Disqus
navigate
go
exit
Дякуємо, що обираєте FREEhost.UA