Пошук уроків, статей та іншого контенту
Розглядаємо поділ системи на шари, їхні залежності, переваги й обмеження для простих застосунків.
Шарова архітектура — це спосіб організації застосунку, у якому код поділяють на рівні відповідальності, або шари.
Кожен шар:
виконує певну роль;
приховує деталі своєї роботи;
взаємодіє з іншими шарами через зрозумілий інтерфейс;
зазвичай залежить від шару, розташованого нижче.
Для простого вебзастосунку часто використовують три шари:
Шар представлення — приймає запити та формує відповіді.
Шар бізнес-логіки — виконує правила застосунку.
Шар доступу до даних — читає та зберігає дані.
Схематично це виглядає так:
Клієнт
↓
Представлення
↓
Бізнес-логіка
↓
Доступ до даних
↓
База даних або інше сховищеНаприклад, для застосунку зі списком завдань:
представлення отримує HTTP-запит GET /tasks;
бізнес-логіка вирішує, які завдання можна повернути;
шар доступу до даних отримує завдання зі сховища;
результат повертається клієнту через представлення.
Цей шар взаємодіє із зовнішнім світом. У вебзастосунку він зазвичай:
приймає HTTP-запити;
читає параметри запиту;
перевіряє базовий формат вхідних даних;
викликає бізнес-логіку;
формує HTTP-відповідь.
Шар представлення не повинен самостійно виконувати складні правила застосунку або працювати із базою даних.
Наприклад, контролер може передати назву нового завдання в сервіс, але не повинен сам вирішувати, чи дозволено створювати порожнє завдання.
Цей шар містить правила, за якими працює застосунок.
Він може:
перевіряти умови операцій;
координувати кілька дій;
обчислювати значення;
вирішувати, коли повертати помилку;
викликати шар доступу до даних.
Бізнес-логіка не повинна залежати від деталей HTTP. Наприклад, сервісу не потрібно знати про req, res, HTTP-коди або заголовки.
Цей шар відповідає за взаємодію зі сховищем:
базою даних;
файлами;
зовнішнім API;
тимчасовим сховищем у пам’яті.
Він приховує технічні деталі зберігання даних. Інші шари можуть викликати метод findAll(), не знаючи, чи дані читаються з PostgreSQL, JSON-файлу або масиву в пам’яті.
У класичній шаровій архітектурі залежності спрямовані зверху вниз:
Представлення → Бізнес-логіка → Доступ до данихНаприклад:
контролер може викликати сервіс;
сервіс може викликати репозиторій;
репозиторій працює зі сховищем.
Але репозиторій не повинен викликати контролер. Також бізнес-логіка не повинна формувати HTTP-відповіді.
Такий напрямок робить структуру зрозумілою: кожен шар знає лише про необхідні йому нижчі рівні.
Нижче наведено невеликий застосунок на Node.js без сторонніх бібліотек. Він має три шари:
TaskRepository — доступ до даних;
TaskService — бізнес-логіка;
HTTP-сервер — шар представлення.
const http = require('node:http');
// Шар доступу до даних
class TaskRepository {
constructor() {
this.tasks = [
{ id: 1, title: 'Вивчити шарову архітектуру', completed: false }
];
}
findAll() {
return this.tasks;
}
create(title) {
const task = {
id: this.tasks.length + 1,
title,
completed: false
};
this.tasks.push(task);
return task;
}
}
// Шар бізнес-логіки
class TaskService {
constructor(taskRepository) {
this.taskRepository = taskRepository;
}
listTasks() {
return this.taskRepository.findAll();
}
createTask(title) {
const normalizedTitle = title.trim();
if (normalizedTitle.length === 0) {
throw new Error('Назва завдання не може бути порожньою');
}
return this.taskRepository.create(normalizedTitle);
}
}
// Створення об'єктів нижчих шарів
const taskRepository = new TaskRepository();
const taskService = new TaskService(taskRepository);
// Шар представлення
const server = http.createServer((request, response) => {
response.setHeader('Content-Type', 'application/json; charset=utf-8');
if (request.method === 'GET' && request.url === '/tasks') {
const tasks = taskService.listTasks();
response.writeHead(200);
response.end(JSON.stringify(tasks));
return;
}
if (request.method === 'POST' && request.url === '/tasks') {
let body = '';
request.on('data', (chunk) => {
body += chunk;
});
request.on('end', () => {
try {
const data = JSON.parse(body);
const task = taskService.createTask(data.title || '');
response.writeHead(201);
response.end(JSON.stringify(task));
} catch (error) {
response.writeHead(400);
response.end(JSON.stringify({ error: error.message }));
}
});
return;
}
response.writeHead(404);
response.end(JSON.stringify({ error: 'Маршрут не знайдено' }));
});
server.listen(3000, () => {
console.log('Сервер запущено на http://localhost:3000');
});Запустити приклад можна командою:
node server.jsПісля цього:
GET /tasks повертає список завдань;
POST /tasks створює завдання;
перевірка порожньої назви виконується в TaskService, а не в HTTP-сервері;
зберігання завдань виконується в TaskRepository.
Наприклад, запит на створення завдання може мати такий вигляд:
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Написати тест"}'Коли код розподілено за відповідальністю, легше знайти потрібне місце для змін:
формат HTTP-відповіді змінюється у шарі представлення;
правило перевірки завдання — у бізнес-логіці;
спосіб зберігання — у шарі доступу до даних.
Бізнес-логіку можна перевіряти окремо від HTTP-сервера та справжньої бази даних.
Наприклад, TaskService може отримати тестовий репозиторій із підготовленими даними. Тоді тест перевіряє саме правила створення завдання, а не роботу мережі чи бази даних.
Якщо репозиторій має зрозумілі методи, сховище можна змінити:
масив у пам'яті → файл → база данихБізнес-логіка при цьому може залишитися без змін.
Правило, яке використовується в кількох місцях, можна розмістити в сервісі. Тоді контролери не копіюють одну й ту саму логіку.
Шарова архітектура не є універсальним рішенням.
Для дуже маленького скрипту окремі класи, файли та шари можуть бути зайвими. Якщо застосунок має лише одну просту операцію, поділ на багато рівнів ускладнить його без помітної користі.
Іноді запит проходить через кілька шарів, хоча кожен із них лише передає дані далі:
контролер → сервіс → репозиторійЯкщо в сервісі немає бізнес-логіки, така структура може створювати зайвий шаблонний код.
З часом сервіс може перетворитися на великий клас, який знає про все. А репозиторій може почати містити бізнес-правила. У такому випадку формальний поділ на шари існує, але фактична структура стає заплутаною.
Реальний застосунок може взаємодіяти з базою даних, чергами повідомлень, зовнішніми сервісами та кешем. Простий поділ на три шари не завжди достатній для складних систем.
Шарова архітектура добре підходить, коли:
застосунок має кілька пов’язаних операцій;
є бізнес-правила, які потрібно відокремити від HTTP;
дані зберігаються або можуть зберігатися різними способами;
над кодом працює кілька розробників;
потрібна базова тестованість і зрозуміла структура.
Для невеликого застосунку не обов’язково створювати складну ієрархію. Часто достатньо трьох простих шарів:
controllers/
services/
repositories/Назви каталогів можуть відрізнятися. Важливий не сам шлях до файлу, а чіткий поділ відповідальності.
Погано, коли контролер сам перевіряє всі правила, працює зі сховищем і формує складні результати.
Краще залишити контролеру лише роботу із запитом і відповіддю, а правила передати сервісу.
Контролер не повинен містити SQL-запити або напряму керувати моделями даних. Це прив’язує HTTP-код до конкретної технології зберігання.
Сервіс не повинен повертати res.status(...) або читати req.body. Його можна викликати з HTTP-контролера, команди командного рядка чи тесту.
Якщо перевірка порожньої назви є і в контролері, і в сервісі, згодом ці перевірки можуть розійтися. Правило краще розміщувати в одному відповідальному місці.
Не потрібно додавати окремий шар лише тому, що це виглядає формально правильно. Кожен шар має виправдовувати своє існування окремою відповідальністю.
Поставте для кожного фрагмента коду просте запитання:
він працює з HTTP-запитом або відповіддю? Це представлення;
він реалізує правило застосунку? Це бізнес-логіка;
він читає або записує дані у сховище? Це доступ до даних.
Якщо клас або функція відповідає одразу на кілька таких запитань, її, ймовірно, варто розділити.
Шарова архітектура ділить застосунок на рівні відповідальності.
Для простих вебзастосунків часто достатньо представлення, бізнес-логіки та доступу до даних.
Залежності зазвичай спрямовані зверху вниз.
Представлення працює з HTTP, сервіс містить правила, репозиторій працює зі сховищем.
Такий підхід покращує зрозумілість, тестованість і можливість заміни окремих частин.
Надмірна кількість шарів може ускладнити маленький застосунок.
Шари мають допомагати розділяти відповідальність, а не створювати зайвий шаблонний код.