Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Модульна архітектура». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Модульна архітектура** ділить застосунок на самодостатні блоки, що спілкуються лише через визначені публічні інтерфейси, приховуючи внутрішню реалізацію від зовнішнього коду. ```javascript // Тільки це видно ззовні модуля: export { UserProfile } from './components/UserProfile.js'; export { useUser } from './hooks/useUser.js'; // userModule/internal/store.js залишається повністю приватним ``` **Ключове:** якщо зміна внутрішніх файлів модуля ламає код поза ним, архітектура не є по-справжньому модульною.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Модульна архітектура** ділить застосунок на незалежні самодостатні блоки (модулі), які спілкуються лише через визначені публічні інтерфейси. Це дає командам змогу розробляти, тестувати і масштабувати кожну частину окремо, не ламаючи решту. ## Теорія ### TL;DR - Аналогія: кухня ресторану, де кожна станція (заготовка, гриль, соуси) має свої інструменти, робить свою справу і передає результат через "вікно видачі" (public API), ніколи не лізучи в шухляди сусідньої станції. - Головний принцип: модулі є чорними скриньками. Зовнішній код бачить лише те, що явно експортовано. - Модулі можуть залежати один від одного, але тільки через публічний `index.js`, ніколи через внутрішні шляхи файлів. - Правило вибору: якщо є кілька команд, функції з незалежним циклом змін або кодова база більша за 10k рядків, модульна архітектура себе виправдовує. ### Швидкий приклад ```javascript // userModule/index.js (PUBLIC API - єдиний вхід) export { UserProfile } from './components/UserProfile.js'; export { useUser } from './hooks/useUser.js'; export { fetchUser } from './api/userService.js'; // App.js (зовнішній код) import { UserProfile, useUser } from './userModule/index.js'; // OK: використовуємо публічний API import { cacheUser } from './userModule/internal/store.js'; // Неправильно: звертаємось до внутрішнього файлу напряму ``` Все, що знаходиться в `userModule/internal/`, це приватна справа модуля. Інші модулі навіть не знають про його існування. ### Головна різниця Модульна архітектура це не просто розкладання файлів по папках. Справжній тест: чи ламає зміна внутрішньої структури одного модуля код інших модулів? Якщо так, маєш організований код, а не модульний. Різниця полягає в **жорстких межах**: модулі взаємодіють лише через явні контракти, тому можна переписати внутрішнє, змінити реалізацію або видалити модуль повністю, не чіпаючи нічого поза ним. ### Коли застосовувати - Кілька команд в одній кодовій базі, де кожна команда відповідає за свій модуль і мержі не конфліктують - Функції з незалежним циклом змін, щоб платежі оновлювались без торкання авторизації - Кодова база, яка переросла одного розробника, бо без меж отримуєш хаос взаємних залежностей - Функціональність, яку можна вмикати і вимикати - Код, який потрібно перевикористовувати у веб і мобільних застосунках ### Як бандлер обробляє межі модулів Коли Webpack або Vite обробляє код, він трасує імпорти від точок входу і будує граф залежностей. Кожен модуль отримує власну область видимості, змінні всередині не потрапляють до глобального простору. Tree-shaking прибирає все, що не виставлено через публічний API. Спроба імпортувати з `userModule/internal/store.js` падає, якщо цей шлях не виставлений в `index.js`. Межа це не просто конвенція. Вона технічно підкріплена. ### Типові помилки **1. Імпорт з внутрішніх папок** ```javascript // Неправильно import { userCache } from './userModule/internal/store.js'; // Правильно import { getUserData } from './userModule/index.js'; ``` Коли власник модуля переробить внутрішнє, твій код зламається. Публічний API це контракт. Внутрішня структура таким контрактом не є. **2. Скорочення через внутрішні файли** ```javascript // paymentModule/components/PaymentForm.js // Неправильно: обходимо публічний API "щоб зекономити час" import { validateToken } from '../../authModule/internal/tokenValidator.js'; // Правильно import { validateToken } from '../../authModule/index.js'; ``` Це створює приховані залежності. На review paymentModule і authModule виглядають незалежними. У продакшені рефакторинг authModule тихо ламає PaymentForm. **3. Циклічні залежності** ```javascript // userModule/api/user.js import { logActivity } from '../../analyticsModule/index.js'; // analyticsModule/api/analytics.js import { getUser } from '../../userModule/index.js'; // Неправильно ``` Бандлери не можуть коректно вирішити циклічні імпорти. Отримуєш `undefined` замість експортів або збій білду. Модулі мають утворювати спрямований ациклічний граф (DAG). Якщо двом модулям потрібна спільна логіка, виноси її в третій. **4. Експортувати все "про запас"** ```javascript // Неправильно: виставляємо внутрішні хелпери export { getUser } from './api/user.js'; export { validateEmail } from './internal/validators.js'; // Внутрішнє! export { userCache } from './internal/store.js'; // Внутрішнє! // Правильно: тільки те, що навмисно публічне export { getUser } from './api/user.js'; ``` Щойно щось виставлено назовні, зовнішній код починає від цього залежати. Змінити вже не вийде без наслідків. `index.js` треба сприймати як публічний контракт. **5. Спільний конфіг як спосіб уникнути дублювання** ```javascript // Module A export const config = { apiUrl: 'https://api.example.com' }; // Module B import { config } from './moduleA/index.js'; // Тепер Module B падає, якщо Module A змінює форму конфігу // Краще: кожен модуль управляє власним конфігом // moduleA/config.js і moduleB/config.js оголошують свій apiUrl окремо ``` Дублювання між модулями краще за приховане зчеплення. ### Де зустрічається в реальних проектах - **React**: feature-модулі (auth, dashboard, settings) кожен експортує компоненти, хуки та сервіси через `index.js` - **Express**: route-модулі (users, payments, admin) експортують роутери, які збираються в `app.js` - **Redux**: feature slices (user, cart, notifications) це незалежні reducer-и зі своїми actions та selectors - **Next.js**: App Router трактує кожен маршрут як самодостатній модуль зі своїм layout і data fetching - **npm-пакети**: кожен пакет це модуль, який використовується через публічний API, ніколи через внутрішні шляхи в `node_modules` Один патерн, який найчастіше приносить проблеми: команди вважають, що спільні TypeScript-типи є приводом імпортувати з внутрішніх файлів. "Це просто тип, нічого не зламається" до того моменту, поки власник модуля не перемістить цей тип і білд розсипається у шести місцях одразу. ### Питання на співбесіді **Q:** Як запобігти циклічним залежностям між модулями? **A:** Використовуй інструменти аналізу графу залежностей, наприклад Madge або Dpdm, в CI, щоб ловити їх до мержу. Проєктуй систему так, щоб залежності йшли в одному напрямку: UI-модулі залежать від feature-модулів, feature-модулі від core/shared, ніколи навпаки. Якщо двом модулям потрібна спільна логіка, виноси її в третій. **Q:** Чим модульна архітектура відрізняється від мікросервісів? **A:** Модульна архітектура існує всередині однієї кодової бази і одного процесу, модулі ділять пам'ять і запускаються разом. Мікросервіси це окремі процеси, що спілкуються через мережу. Почати з модульної архітектури простіше. Мікросервіси додають складність деплойменту, але дають незалежне масштабування. **Q:** Як управляти спільним станом між модулями без зчеплення? **A:** Використовуй шар управління станом (Redux, Zustand, Context), яким не володіє жоден модуль напряму. Модулі відправляють дії і підписуються на зрізи стану. Або використовуй event emitter: модулі генерують події, інші слухають, не знаючи хто саме. Уникай прямого імпорту об'єктів стану між модулями. **Q:** Модулі можуть залежати один від одного напряму, або все через центральний хаб? **A:** Прямі залежності нормальні, якщо вони ациклічні. A залежить від B, B від C це добре. A від B, B від A вже ні. Центральний хаб (Redux store або event bus) теж підходить, але створює вузьке місце: кожен модуль мусить про нього знати. **Q (рівень senior):** Як мігрувати кодову базу з "все залежить від усього" до модульної структури без зупинки продакшену? **A:** Почни з визначення природних доменних меж: auth, payments, users, notifications. Створи файли `index.js` з публічними API, не переміщаючи жодного внутрішнього файлу. Поступово переводь імпорти з внутрішніх шляхів на публічні API. Додай lint-правило, яке забороняє прямі імпорти з `*/internal/*`. Роби це паралельно зі звичайною роботою над фічами, не намагайся переписати все за один спринт. ## Приклади ### Базова структура модуля в React ```javascript // userModule/index.js - публічний API, єдиний файл для зовнішніх імпортів export { UserProfile } from './components/UserProfile.js'; export { useUser } from './hooks/useUser.js'; export { fetchUser } from './api/userService.js'; // userModule/internal/store.js - приватний, не виставляється з index.js const userCache = new Map(); export function cacheUser(id, data) { userCache.set(id, data); } // userModule/api/userService.js - приватний, використовується тільки всередині import { cacheUser } from '../internal/store.js'; export async function fetchUser(id) { const response = await fetch('/api/users/' + id); const data = await response.json(); cacheUser(id, data); // стратегія кешування - внутрішня деталь return data; } // App.js - зовнішній код import { UserProfile, useUser } from './userModule/index.js'; // fetchUser теж доступний. cacheUser і userCache - ні. ``` Якщо команда вирішить замінити `Map` на Redis-клієнт, жодний зовнішній файл не зміниться. ### Запобігання циклічним залежностям ```javascript // paymentModule/index.js export { PaymentForm } from './components/PaymentForm.js'; // paymentModule/components/PaymentForm.js import { notifyUser } from '../../notificationModule/index.js'; // OK: імпорт з публічного API іншого модуля // notificationModule/api/notify.js // Неправильно: циклічна залежність назад до paymentModule: // import { PaymentForm } from '../../paymentModule/index.js'; // Правильно: notificationModule нічого не знає про paymentModule export function notifyUser(message, type) { console.log('[' + type + '] ' + message); } // Payments залежить від notifications. Notifications не залежить від payments. ``` Кожен тестується окремо, і жоден не може випадково зламати білд іншого. ### Поступова міграція до модульних меж ```javascript // До: прямі внутрішні імпорти розкидані по всьому проекту import { validateToken } from './authModule/internal/tokenValidator.js'; import { userCache } from './userModule/internal/store.js'; // Крок 1: створюємо публічні API, не переміщаючи файли // authModule/index.js export { validateToken } from './internal/tokenValidator.js'; // userModule/index.js export { getUserFromCache } from './internal/store.js'; // Крок 2: оновлюємо імпорти до публічних API import { validateToken } from './authModule/index.js'; import { getUserFromCache } from './userModule/index.js'; // Крок 3: додаємо lint-правило, що забороняє внутрішні імпорти // .eslintrc: заборонено прямі імпорти з шляхів */internal/* ``` Кожен крок маленький і перевіряється окремо. Не потрібен цілий спринт, щоб переписати все відразу.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.