Пошук уроків, статей та іншого контенту
Порівняєте монолітний і модульний підходи та визначите межі відповідальності компонентів системи.
Архітектура описує, з яких частин складається система, за що відповідає кожна частина та як вони взаємодіють.
Наприклад, інтернет-магазин може містити такі компоненти:
каталог товарів;
кошик;
замовлення;
оплата;
сповіщення;
автентифікація користувачів.
Архітектура допомагає відповісти на запитання:
де реалізувати певне правило;
який компонент має змінювати дані;
як компоненти взаємодіють;
наскільки зміна одного компонента вплине на інші.
У цьому уроці розглянемо два підходи:
монолітну архітектуру;
модульну архітектуру.
Монолітна система — це система, яку створено та зазвичай розгортають як один застосунок.
Усі основні частини можуть перебувати в одному репозиторії, використовувати одну базу даних і запускатися одним процесом.
Наприклад, структура монолітного застосунку інтернет-магазину може виглядати так:
shop/
├── server.js
├── users.js
├── products.js
├── orders.js
├── payments.js
└── notifications.jsТе, що код розділено на файли, ще не робить систему модульною. Важливо, чи мають ці частини чіткі межі та чи залежать вони безпосередньо від внутрішньої реалізації одна одної.
Для невеликої системи моноліт часто є практичним вибором:
його простіше створити на початку;
його легко запустити локально;
не потрібно налаштовувати взаємодію між окремими сервісами через мережу;
простіше налагоджувати один застосунок;
можна виконувати операції в межах однієї транзакції бази даних.
Наприклад, команда з кількох розробників може швидше створити першу версію магазину як один застосунок.
З часом моноліт може стати складним для підтримки:
компоненти починають напряму змінювати дані одне одного;
зміна в одному місці ламає інші частини;
складно визначити, де саме має бути бізнес-логіка;
тестування потребує запуску великої кількості коду;
різні частини системи не можна незалежно змінювати або розгортати.
Проблема не в самому факті, що застосунок є одним процесом. Проблема виникає тоді, коли його частини не мають зрозумілих меж.
Модульна архітектура розділяє систему на модулі. Кожен модуль відповідає за окрему предметну область або функцію.
Наприклад:
shop/
├── app/
│ ├── orders/
│ │ ├── orderService.js
│ │ └── orderRepository.js
│ ├── payments/
│ │ ├── paymentService.js
│ │ └── paymentRepository.js
│ └── notifications/
│ └── notificationService.js
└── server.jsУ цьому прикладі:
модуль orders працює із замовленнями;
модуль payments відповідає за оплату;
модуль notifications надсилає сповіщення;
кожен модуль приховує власну внутрішню реалізацію.
Модулі можуть перебувати в одному застосунку та запускатися одним процесом. Такий підхід часто називають модульним монолітом.
Отже, моноліт і модульність не є протилежностями:
моноліт описує спосіб пакування та розгортання системи;
модульність описує організацію коду та межі між його частинами.
Система може бути одночасно монолітною та добре модульною.
Межа відповідальності визначає, за що відповідає компонент і чого він не повинен робити.
Наприклад, модуль замовлень може відповідати за:
створення замовлення;
перевірку складу замовлення;
зміну статусу замовлення;
отримання інформації про замовлення.
Він не повинен:
реалізовувати спосіб списання коштів;
формувати електронний лист;
безпосередньо змінювати внутрішні дані модуля оплати.
Модуль оплати, своєю чергою, відповідає за:
перевірку платежу;
списання коштів;
повернення платежу;
збереження статусу платежу.
Для кожного компонента корисно сформулювати одне речення:
Цей компонент відповідає за ...
Якщо речення виходить надто загальним, компонент, імовірно, має забагато відповідальності.
Наприклад:
добре: «Модуль замовлень відповідає за життєвий цикл замовлення»;
погано: «Модуль замовлень відповідає за замовлення, оплату, листи та користувачів».
Також перевірте, хто володіє певними даними:
модуль замовлень володіє статусом замовлення;
модуль оплати володіє статусом платежу;
модуль користувачів володіє даними профілю.
Інші модулі можуть запитувати ці дані через публічний інтерфейс, але не повинні напряму змінювати внутрішнє сховище іншого модуля.
Модуль має приховувати деталі реалізації та надавати обмежений публічний інтерфейс.
Наприклад, замість прямого доступу до всіх властивостей замовлення можна надати функцію createOrder. Інші частини застосунку викликають цю функцію, але не знають, як саме замовлення зберігається.
// orderModule.js
function createOrder({ userId, items }) {
if (!userId) {
throw new Error('Потрібен ідентифікатор користувача');
}
if (!Array.isArray(items) || items.length === 0) {
throw new Error('Замовлення має містити товари');
}
const order = {
id: `order-${Date.now()}`,
userId,
items,
status: 'created'
};
return order;
}
function markAsPaid(order) {
if (order.status !== 'created') {
throw new Error('Оплатити можна лише нове замовлення');
}
return {
...order,
status: 'paid'
};
}
module.exports = {
createOrder,
markAsPaid
};// paymentModule.js
function payForOrder(order) {
if (order.status !== 'created') {
throw new Error('Платіж неможливий для цього статусу замовлення');
}
return {
orderId: order.id,
status: 'successful'
};
}
module.exports = {
payForOrder
};// app.js
const orderModule = require('./orderModule');
const paymentModule = require('./paymentModule');
const order = orderModule.createOrder({
userId: 'user-42',
items: [
{ productId: 'book-1', quantity: 1 },
{ productId: 'pen-2', quantity: 2 }
]
});
const payment = paymentModule.payForOrder(order);
const paidOrder = orderModule.markAsPaid(order);
console.log('Платіж:', payment);
console.log('Замовлення:', paidOrder);Щоб запустити приклад:
створіть три файли: orderModule.js, paymentModule.js і app.js;
вставте відповідний код;
виконайте команду:
node app.jsМодуль оплати не змінює замовлення напряму. Він повертає результат оплати, а модуль замовлень сам змінює статус замовлення через власну функцію markAsPaid.
Це важлива властивість меж: кожен модуль змінює власний стан за власними правилами.
Модулі неминуче взаємодіють, але взаємодія має бути контрольованою.
Наприклад, типовий процес може виглядати так:
модуль замовлень створює замовлення;
застосунок передає замовлення модулю оплати;
модуль оплати повертає результат;
модуль замовлень змінює статус замовлення;
модуль сповіщень отримує повідомлення про новий статус.
Необов’язково, щоб модуль замовлень знав усі деталі платіжної системи. Йому достатньо знати, яку функцію викликати та який результат очікувати.
Зчеплення — це міра залежності компонентів один від одного.
Сильне зчеплення виникає, коли:
один модуль імпортує багато внутрішніх деталей іншого;
модулі спільно змінюють один і той самий об’єкт;
модуль напряму працює з таблицями іншого модуля;
зміна назви внутрішньої властивості змушує змінювати багато файлів.
Слабше зчеплення виникає, коли:
модулі взаємодіють через невеликий публічний інтерфейс;
внутрішні дані модуля приховані;
модуль передає іншому лише необхідні дані;
правила зміни стану зберігаються всередині модуля-власника.
Мета модульної архітектури — не усунути всі залежності, а зробити їх зрозумілими та контрольованими.
Монолітний застосунок:
зазвичай розгортається як одна одиниця;
простіший для першого запуску;
може мати спільну базу даних;
з часом може стати важким для змін, якщо межі між частинами нечіткі.
Модульний застосунок:
складається з компонентів із визначеними відповідальностями;
приховує внутрішню реалізацію модулів;
зменшує кількість випадкових залежностей;
полегшує тестування та подальші зміни;
може залишатися одним застосунком і одним процесом.
Для початку проєкту часто достатньо модульного моноліту. Він дає простоту розгортання моноліту та зрозумілу структуру модульної системи.
Під час проєктування можна скористатися таким алгоритмом:
Випишіть основні функції системи.
Об’єднайте функції, які працюють з однією предметною областю.
Для кожної групи визначте одну основну відповідальність.
Визначте, які дані належать кожному модулю.
Сховайте внутрішні дані та реалізацію.
Опишіть невеликий публічний інтерфейс.
Перевірте, чи не знає модуль зайвих деталей про інші модулі.
Наприклад, для магазину можна отримати такі межі:
users — користувачі та їхні профілі;
products — товари та ціни;
orders — замовлення та їхні статуси;
payments — платежі та їхні результати;
notifications — надсилання сповіщень.
Якщо модуль починає відповідати за кілька різних предметних областей, його варто переглянути та, можливо, розділити.
Файл app.js, який одночасно обробляє користувачів, замовлення, оплату та сповіщення, швидко стає складним.
Краще розділяти код за відповідальністю, а не лише за типом файлу.
Структура на кшталт:
controllers/
services/
repositories/
models/може бути корисною, але сама по собі не визначає межі предметних областей.
Якщо логіка замовлень розкидана по всіх цих каталогах без зрозумілої групи orders, підтримувати її складніше.
Якщо модуль замовлень напряму змінює таблицю платежів, правила платежів більше не належать модулю оплати.
Краще викликати публічну операцію модуля оплати, наприклад payForOrder.
Якщо модуль експортує всі свої функції та внутрішні об’єкти, інші частини системи починають залежати від деталей реалізації.
Експортуйте лише те, що справді потрібно іншим модулям.
Окремі файли або каталоги ще не гарантують модульності. І навпаки, один процес може містити добре ізольовані модулі.
Важливі не кількість процесів і не назви каталогів, а відповідальності, дані та правила взаємодії.
Моноліт — це система, яку зазвичай розгортають як один застосунок.
Модульна архітектура ділить систему на компоненти з чіткими відповідальностями.
Моноліт може бути модульним.
Кожен модуль має володіти своїми даними та правилами їх зміни.
Взаємодія між модулями має відбуватися через невеликий публічний інтерфейс.
Для невеликих систем модульний моноліт часто є простим і практичним рішенням.
Якісні межі компонентів зменшують кількість залежностей і спрощують подальші зміни.