Кожен хто займається розробкою певний час приходить до слова архітектура. Сьогодні ми подивимось на одну з парадигм проектування архітектури через призму типового JS бекенду.
Все почалось коли стало погано
В ідеальному світі ми розробники з гарною освітою знаємо про архітектуру заздалегіть і всі ми перед початком розробки проекту обираємо архітектурний принцип і дотримуємось його весь життевий цикл проекту. Але ми не в ідеальному світі. Не всі з нас мають структуровану освіту. Як мінімум половина сучасних розробників не має профільної освіти, вони випускникі курсів, або самоучкі. І коли ти самоучка питання архітектури проходить повз тебе. Тобі більш важливо навчитися виконувати прості задачі щоб отримати свою першу роботу.
Також часто ми приходимо на проект який вже розробляється певний час. Хтось його писав так як вміє і щоб воно відповідало вимогам клієнта, але найчастіше не відповідало вимогам архітектури (питання холіварне і ви можете сказати що то просто мені так не везе, а навколо вас всі розробники досвідченні і роблять гарну архітектуру, але камон…)
Кожен з нас стикався з ситуацією коли проект з кожною новою фічею ставав важчим для змін. Коли одне виправлення без регресивного тестування всього проекту приводило к багам на продакшені в неочікуваних місцях. І коже з нас думав що цей проект треба переписувати з нуля
Десь в цей момент багато хто починає цікавитись архітектурою додатків. Бо всі ми бажаємо уникнути помилок минулого. І знайти інструмент чи мануал який допоможе нам легко і швидко виконувати задачі отримуючи задоволення від життя
Тепер він в мене є
Архітектура - це набір правил для проектування сервісів і технологій. Який допомагає систаматезувати часто використовуємі практики.
В інтернеті дуже багато відео і статей на тему архітектури додатків. Цих архітектур ціла купа. І це перший очевидний висновок. Ідеального варіанту немає. Не придумали люди такої архітектури яка закривала б усі потреби і однаково ефективно працювала для всіх задач. Да і не придумають ніколи(напевно). А це означає що нам треба обрати той що підходить для наших задач.
Як я вже казав видів архітектур достатньо багато, якісь розповсюджені тільки для певних технологій. Якісь мають більш загальний вид і підходять як для одної технології так і до іншиї.
Сьогодні ми поговоримо про один з видів архітектури поширений в розробці на C# або android. Але ми спробуємо його використати для JS бекенду, бо цей підхід має загальні рекомендації і не сильно прив'язаний до певної мови програмування.
Чиста Архітектура
Цей термін отримав широке поширення завдяки Роберту Мартіну. Він має одноіменну книгу, але вона мало відноситься до того що більшість сучасних розробників розуміють під цим терміном. А розуміють вони саме ті принципи проектування які він навів у свої статті з такою назвою. Ознайомитись з нею можно ось тут. Стаття невелика, і є концентрацію ідей про те як варто будувати додатки.
Автор статті пропонує розділити додаток на декілько шарів.
Frameworks
Interface Adapters
UseCases
Entity
Frameworks - відповідальний за зовнішню взаємодію. Тобто тут знаходиться те що описує взаємодію з користувачем нашого додатку. Для бекенда такою взаємодією будуть виклики з фронта, або інших сервісів, доя фронта це буде UI складова роботи з юзером
Interface Adapters - це шар відповідальний за передачу данних до наших use cases і потім за перетворення отриманих відповідей для запитуючого. Тут знаходяться контроллери і презентори
UseCases - це шар на якому знаходяться сценарії виростання.
Entity - це шар який акамулює всі бізнес правила стосовно сутностей нашого додатку
Одне з найважливіших правил яке надає ця архітектура це правило залежностей модулів один від одного. Напрямок залежностей повинен йти в зворотній бік. Тобто Entity не залежить невідкого, UseCases залежить від Entity, Interface Adapters залежить від UseCases, Frameworks залежить від Interface Adapters
Куди я попав?
Так склалось що у вільному доступі дуже мало інформації про адаптації подібного підходу в JS бекенд. Але багацько прикладів є в бекендах на С#(не тільки в бекендах). З цього ми можемо зробити висновок що ця архітектура підходить для бекенда. Для експеримента я обрав Nest.js. Чому? Тому що в ньому вже є реалізація DI і нам не доведеться реалізовувати це власноруч(Ну і я просто люблю Nest.js і вважаю його найкращім фреймворком для створення бекенда)
❗️ Disclaimer: Все наведене нижче це експеримент. Ви можете його модернизувати, але не раджу використовувати все це без застережно на production
Почнемо з самого головного
З folder structure. Так як вся ця архітектура прийшла з компільованих мов програмування, то кожен з шарів відокремлювався в окремий проект, який компілювався окремо. Саме це дає один з переваг архітектури. Дає їй певну стійкість. А також певну ізоляцію. В нас же мова програмування інтерпретована. І тому ми вільні від проблеми пересборки проекту. Але зберегти цю гарну структуру не буде зайвим. Дозволяє тріщки більше часу подумати над змінами які змушують нас редагувати декілька шарів.
Папки для Framework layer в нас немає. Чому? Справа в тому що саме цей шар відповідає за зовнішню взаємодію нашого додатку. Якщо б ми писали на чистому node.js ми б створювали подібну папку в якій би описували процес передачі запитів на наші контроллери. Але так як нес робить це за нас нам немає необхідності створювати подібну папку.
Поїхали далі. Domain - це папка для шару Entity. Тут зберігаються всі дані про наші сутності(Entity, Value Object, Exception, Repository Interfaces).
Application - це папка для шару UseCases. Тут будуть зберігатись сервіси які будуть реалізовувати use cases. Тобто сценарії використання.
Infrustructure - це папка для шару Interface Adapters. Тут будуть зберігатись все що пов’язано з роботою з базою наприклад, або з сторонніми сервісами
Presentation - це теж папка яка є частиною Interface Adapters Layer. Тут будуть наші контроллери які будуть викликати сервіси із Application Layer. А також тут можуть бути презентори.
Ось це 4 корневі папки які ми будемо використовувати для розділенні нашого кода. В сумі маємо 2 папки на Interface Adapters Layer, 1 для Entity Layer і одну на Application Layer
Знов todo?
Так знов. Це найкращій спосіб щось зрозуміти не зосереджуючись на деталях предметної області.
Подивимсь на те як виглядає Entity Layer. Як я вже казав трошки раніше цей шар застосунку відповідає за реалізацію бізнес сутностей. Це такі сутності які уособлють собою предметну область нашого проекту. Наприклад одною з корневих Entity для інтернет магазину є продукт. Тобто якби ми робили інтернет магазин в цій папці в нас знаходився файл з описом сутності “продукт”. Так як ми робимо ToDo ліст то тут в нас будуть знаходитись todo-item
Так виглядає folder structure. Нічого зайвого чого не потрібно знати нашій сутності. Що тут ще може бути окрім сутностей? Наприклад shared компоненти які загальні для всіх entity
Тепер подивимось на реалізацію найголовнішого що в нас є на цьому шарі на самому класі сутності.
Ось так виглядає наш entity. Також тут ви можете побачити кастомні ексепшени. Вони знаходяться теж на цьому рівні. Тільки в тому випадку коли вони стосуються entity. Ось так вони виглядають.
Ще одним важливим моментом є те що всі задачі по зміні стейта всередині entity повинен займатись тільки сам entity. Ніхто зовні не повинен впливати на зміни всередині.
Занадто просто? Можливо, але це дозволяє робити максимально стійким частину нашого коду яка відповідає за бізнес правила. А це є дуже важливий плюс як для бізнесу так і для розуміння проекту новими розробниками. А також його просто тестувати.
Application
Переходимо до другого леєра. Тут в нас знаходяться наші useCases. Тут будуть сервіси які будуть реалізовувати всі сценарії взаємодії з нашими ентіті.
От як виглядає folder structure. Тут в нас є папка з сервісами, Папка shared в якій знаходяться сутності які повсемістно будуть використовуватись на цьому леєрі. Також папка репозиторії в якому будуть зберігатись інтерфейси для репозеторіїв які буде використовувати наші сервіси. Окрім цього є папка для DTO, а також папка з специфікаціями. Якщо хочете більше дізнатись про специфікації, можете почитати ось цю мою статтю. Тут використовується саме така реалізація цього паттерну
Подивимось що в нас відбувається у в головному класі цього шару. В класі сервіса.
Клас вийшов доволі великий, але тут реалізована більшість основних методів щоб можно було подивитись як це працює на практиці.
Почнемо з конструктора, тут ми використовуємо механізм інжекта залежностей який подробніше описаний тут. Тобто ми інжектуємо репозиторій, інтерфейс якого описаний на цьому леєрі.
В більшості функцій що ми тут бачимо реалізується певний патерн дій. Якщо нам необхідно отримати певні данни з бази ми робимо туди запит але через репозиторій, не напряму. Отримуємо ентіті. Потім за допомогою функцій самого класу ентіті ми робимо певні дії і зберігаємо зміни в базі. Важливо, ми не працюємо з базою напряму, тільки через репозиторій. А також ми не змінюємо данні в ентіті напряму. Тільки за допомогою функцій самої сутності. Таким чином на данному сервісі ми просто реалізуємо послідовність викликів інших сервісів і функцій сутностей.
Infrustructure
В цьому шарі все максимально просто. Схеми для бд, репозиторії які реалізують інтерфейси з Application. Також тут можуть бути сервіси зовнішньої взаємодії, наприклад сервіс для відправки email
Як ви бачите тут також присутні кастомні ексепшени, які відносяться до цього леєру. Ось як виглядає репозиторій для todo-item
Більше інформації по реалізації подібного репозиторію також можно знайти тут. Тут я дуже детально розповідати не буду, тільки зазначу те що за перетворення відповіді бд на об’єкти ентіті відповідальний також репозиторій.
Presentation
Переходимо до останньої папки в нашому проекті. Тут в нас все теж достатньо просто. Тут в нас тільки контроллери зараз, але також тут будуть знаходитись презентори якщо це потрібно
Ну і сам контроллер не є чимось складним просто виклик необхідної функції з сервісу
Залежності
Як я казав на початку одна з ідей цієї архітектуру це залежності в проекті. На картинці напочатку напрямок залежностей йде з зовні в середину. Тобто Domain Layer який в середині не залежить не від кого. Далі Application залежить від Domain і тільки. Далі Infrustructure і Presentation залежать від Application, а значить і залежить від Domain.
Щоб зберегти напрямок залежностей для Infrustructure ми використали механізм dependency inversion. Таким чином ми можемо використовувати репозиторії у application layer, але application layer не залежить від шару інфраструктури.
Тестування
Плюсом данної архітектури є її велика тестопригодність. Кожен окремий модуль, кожну окрему функцію можно дуже легко тестувати, використовуючи моки. Таким чином досягається дуже великий рівень покриття коду тестами. А це дуже добре як для нас так і для замовника.
Висновки
Підводячи підсумок, можу сказати що цей підход як на мене пає великий потенціал також і в розробці на JS. Він є стійким, легко тестуємим, легким для змін, а також легко читаємим для нових розробників. Те що ми розділяємо обов'язки між шарами дозволяє нам запобігти змішанню відповідальності. Тому можу рекомендувати спробувати цей підхід на власних проектах)