Архітектура модульного моноліту у Vue-застосунку

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Вітаю, я Олександр Микулич, Frontend Guild Master у TENTENS Tech by SKELAR. Архітектура frontend-частини нашої платформи за час існування зазнавала значних змін. Бізнес неодноразово змінював вектор, команда масштабувалась, а необхідність у швидких поставках нових фіч збільшувалась. Відповідаючи на ці виклики, ми прийшли до архітектури модульного моноліту. Сьогодні хотів би поділитися нашим досвідом.

Що таке архітектура

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

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

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

Ми часто думаємо, що треба включити у наш архітектурний документ:

  • Чи важливий нам час збірки проєкту?
  • Чи важлива для нас кількість людей у команді?
  • Чи важлива кількість користувачів?
  • Чи важливий СІ/CD-pipeline?

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

Але як зрозуміти, що є важливим, а що ні? Як зрозуміти, що всі важливі речі описано? Коли ми можемо сказати, що наша архітектура є хорошою?

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

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

Використання модульного моноліту ми пропускали через такі самі фільтри, але починалось все простіше.

Vue-застосунок. З чого ми починали

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

Наша платформа написана на Vue 2 і на початку мала доволі стандартну структуру. Нижче пропоную розглянути одну з можливих варіацій такої структури:

Якщо ви не знайомі з Vue — не страшно, оскільки модульний моноліт — це концепція, яка не прив’язана до бібліотеки чи фреймворку, знання vue не є обов’язковим. Але якщо ви хочете ближче познайомитись із цим фреймворком і пограти з шаблонами проєктів, то можу рекомендувати таку послідовність:

  1. Почати з офіційної документації та з шаблону, представленого там. Він дає хороше уявлення про Vue і його основну екосистему бібліотек.
  2. Потім спробувати vitesse від Anthony Fu. Це достатньо production-ready-шаблон, який містить багато готових налаштувань і розширить ваше розуміння Vue-екосистеми.
  3. Далі можна спробувати nuxt і їхній офіційний шаблон проєкту.
  4. І vitesse-nuxt3 від того самого Anthony Fu.

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

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

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

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

Проблема зростання

Деякі дані про нас на поточний момент:

  • вісім кросфункціональних команд, кожна відповідає за свою зону;
  • 23 frontend-спеціалісти та найближчим часом плануємо вирости ще;
  • приблизно п’ять-дев’ять деплоїв на продакш за день з повним SDLC-процесом.

Один з викликів, який постає перед багатьма проєктами, — це висока зв’язність кодової бази, або high coupling.

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

  • Low coupling High cohesion;
  • GRASP, SOLID, DDD;
  • Dependency Injection, Inversion of control;
  • Vertical Slice Architecture, Modular Monolith.

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

Основна ідея проста: розбиймо наш застосунок на певні частини (модулі) і зведемо взаємодію між цими частинами до оптимального мінімуму. Залишилось розібратись, що це за «модулі» і як досягти цього «оптимального» мінімуму.

Модульний моноліт

Спрощено модульний моноліт можна представити наступним чином:

Частини, на які ми розбиваємо наш застосунок, ми звемо модулями. Модуль — це відокремлена частина, яка інкапсулює у собі:

  • конкретну функцію;
  • можливість;
  • доменну проблему.

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

Окремою складовою системи є інфраструктура застосунку — це фундамент, на якому перебувають і до якого під’єднуються наші модулі.

Інфраструктура відповідає за:

  • забезпечення базової роботи застосунку (управління DI-контейнером, роутинг, глобальний стан, глобальні сервіси, глобальна шина подій тощо);
  • загальні бізнесові контексти, які доступні всім модулям (наприклад, профіль користувача, стан фіча-флагів чи активні A/B-тести);
  • спільні компоненти чи утиліти.

Окремо варто згадати про взаємодію між модулями. Тут на допомогу приходять різні механізми та підходи:

  • event based-підходи (eventBus, Vue events тощо): дозволяють досягти найслабшого зв’язування модулів;
  • dependency injection, inversion of control: практики з керування та ін’єкції залежностей, як infra -> module, так і module -> infra;
  • зміна глобального стану чи робота з глобальними сервісами: створює контекстуальну залежність;
  • Vue slots — один зі способів гнучкої композиції компонентів з різних модулів;
  • пряме використання модуля іншим модулем: важливо зазначити, що стараємось зводити такий тип взаємодії до мінімуму.

Залежно від ситуації вибираємо той чи інший підхід.

Переваги та виклики

Які ж переваги ми відчули з модульним монолітом?

  • Простота, хороший Developer Experience. Одна кодова база — це просто, а чітке розбиття на модулі допомагає керувати складністю в межах цієї бази.
  • Консистентність. Усе розміщено в одному місці, тому розходження типів, API, версій бібліотек та іншого зведено до мінімуму.
  • Легший рефакторинг. Завдяки перевагам, описаним вище, і компонентним тестам ми отримали можливість безпечного рефакторингу нашої системи.
  • Відсутність Merge Hell. Кожен модуль належить до зони відповідальності однієї з команд, внаслідок цього необхідність працювати з одним кодом різним командам зведена до мінімуму.
  • Простий деплой і розгортання. З цим у моноліту ніколи не було проблем, зі свого боку модулі на це сильно не впливають.

Саме ці переваги у поєднанні з хорошим CI/CD-процесом дозволяють нам робити п’ять-дев’ять деплоїв на продакшн кожного дня.

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

  • Неявність. Якщо звернути увагу на способи міжмодульної взаємодії, то майже всі вони мають цей суттєвий недолік. Тут на допомогу приходить документація, автотести та постійне підсвічування цієї проблеми.
  • Недостатня гнучкість. Ми тримаємо у фокусі та розуміємо, що з подальшим ростом нашої команди та виділенням зон може знову виникнути проблема того, що у моноліті нам стане затісно. Тому тут пам’ятаємо про інші підходи зменшення зв’язності: полірепозиторій чи пакетний монорепозиторій, мікрофронтенди чи iframe.
  • Ризик сильної зв’язності модулів. Модулі — це не срібна куля, і якщо їх доменні зони розбиті некоректно, це може призвести до сильної їх зв’язності, а це саме та проблема, з якою ми й боролись. Тут потрібно визнати помилку, переосмислити та передизайнити зони відповідальності цих модулів.
  • Мегамодуль. Як і в попередньому пункті, через неправильне розуміння домену можливо, що наш модуль стане занадто великим і буде брати на себе забагато відповідальності. Тож це привід передизайнити модуль.

Висновки

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

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

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

👍ПодобаєтьсяСподобалось21
До обраногоВ обраному13
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі