Рефакторинг додатку за MVC архітектурою

У backend розробці прийнято не писати весь код в одному файлі, а розділяти його на блоки в залежності від функціонала. Реалізують це у вигляді MVC-архітектури (модель-вид-контроллер). Тобто розділяють бізнес-логіку і презентацію.

Модель - це блоки роботи з базами даних і різні серверні обробки

Вид - блоки, які комунікують із зовнішнім світом. (повернення json, route, html тощо). Тобто те, що віддаємо назовні.

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

Проведемо рефакторинг за визначеною архітектурою код розглянутий раніше.

Створимо проєкт такої структури:

project
      ├── index.js
      ├── package.json
      ├── usersdb.json
      ├── enviroments
      │             ├── development.env
      │             └── production.env
      ├── models
      ├── controllers
      │             └──userController.js
      ├── middlewares
      │             └──userMiddlewares.js
      ├──routes
      │       └──userRoutes.js
      ├──views
      ├──utils
      │      ├──appError.js
      │      ├──catchAsync.js
      │      └──index.js
      └──servises

index.js - головний файл роботи сервера.

package.json - файл конфігурації проєкту.

usersdb.json - файл, який тимчасово поки виконує роль бази даних.

enviroments - каталог в якому зберігаються файли зі змінними оточення.

models - моделі даних для збереження в базу даних

controllers - каталог, який містить контролери обробки запитів.

middlewares - каталог, який містить проміжне пз.

routes - каталог, який містить роути запитів.

views -

utils - каталог із допоміжними функціями (ваділатори, обробники тощо).

servises - каталог зі стороннім функціоналом (бази даних, функції-конфігуратори, розсилки тощо.

Файли прийнято називати у форматі camelCase (наприклад userController.js), інколи модна побачити синтаксис через крапку (наприклад user.controller.js)

Винесення функцій-обробників (Controllers)

З головного файлу index.js винесемо окремо в userController.js обробники запитів. розглянемо на прикладі створення користувача.

Переносимо в цей файл використовувані бібліотеки

А також саму функцію контроллера (назвемо її, наприклад createUser):

В index.js імпортуємо контролери й підключаємо до роуту

Після кожного невеликого рефакторингу не забуваємо перевіряти чи не порушився функціонал.

За аналогією виносимо всі інші контролери також у файл userController.js. Назви їм дамо, наприклад такі: getAllUsers, getOneUser, updateUser, deleteUser.

Нижче розгорни, щоб побачити весь код цього файлу.

Контроллери винесені в окремий файл

А в нашому index.js лишаються гарні лаконічні роути:

А також після них пропишемо обробку запиту за неіснуючим роутом і виникнення непередбачуваної помилки сервера.

Розгорни, щоб побачити

Роутери (Router)

Як ми побачили вище, в index.js лишилися обробники роутів користувача. Але сервер може обробляти запити не тільки користувача, але й інші запити й згодом їх перелік стане довгим. Видно, що спільним для них є базовий запит users. У express передбачена можливість винесення однотипних роутів завдяки Router. Підключаємо і створюємо екземпляр роута:

Переносимо імпорт контроллерів з index.js до userRoutes.js

Переносимо всі роути з index.js до userRoutes.js і замість app вказуємо router. Крім того прибираємо базовий урл users, бо його ми вкажемо в index.js. І експортуємо наш роутер:

Після цього нам потрібно в index.js заімпортувати роути повʼязані з користувачами і вказати який роут оброблятиме запити за users (синтаксис як у middleware):

ПОКРАЩЕННЯ КОДУ: У файлі роутів можна деструктуризувати наші контроллери і відтак упростити код для читання.

Альтернативний синтаксис роутів через метод route:

Винесення проміжних функцій (Middlewares)

Проміжні функції (middleware) також доречно виносити в окремі файли. Наприклад ми стоврювали в index.js кастомний middleware отримання обʼєкту користувача. Давайте його винесемо в окремий файл userMiddlewares.js. Порядок такий само як і при роботі з контроллерами. Підключаємо у файлі необхідні модулі і створюємо функцію та експортуємо її.

Утім підключати цю функцію ми будемо не в index.js, а в userRoutes.js, адже саме на цьому маршруті використовується ця проміжна функція.

У роуті ми перераховуємо по порядку всі middleware, а останнім вказуємо контроллер обробки запиту. Примітно, що цю проміжну функцію треба використовувати тільки там де в маршруті присутній id:

Або можна прописати цей middleware перед тим як будуть опрацьовані роути, де він використовується.

РЕМАРКА: Припустимо у нас є в системі кроистувач, який обробляється не за правилами за запитом маршруту users/bad, у разі, якшо ми його пропигемо вкінці, то він проігнорується. Тому кастомні маршрути, щоб вони опрацювалися, потрібно прописувати перд тими роутами в яких обробка іде за подібною схемою з динамічним параметром у запиті.

Кастомний обробник помилок

Можна написати клас специфічної обробки помилок.

Для цього створимо на базі класу Error наш клас AppError. Він прийматиме статус помилки і повідомлення:

Для полегшення підключення цих модулів створимо в папці utils файл index.js, який буде хабом для модулів в цій папці:

Для наочності використання цього класу застосуємо його в нашому кастомному middleware. Підключаємо:

Пропишемо всередині якусь перевірку. наприклад на довдину id, щоб вона була не короштою за 10 символів, інакше згенеруємо помилку:

В такому разі помилку буде опрацьовувати глобальний хендлер помилки. Для цього потрібно просто замість повернення статусу помилки в секції case(err) {} передати естафету функції next і як параметр передати їй помилку.

Файл кастомного middleware після змін

Тоді в обробнику помилки в index.js потрібно прописати повернення нового коду помилки і повідомлення, або за замовчанням статус 500.

Перевіримо відпрацювання цієї помилки:

Рефакторинг повторюваних паттернів try{}catch(){}

Як бачимо у наших проміжних обробниках і контроллерах постійно повторюється конструкція try{}catch(){}. Підхід Don't repeate yourself каже, що цей код треба покращити.

Напишемо в utils в файлі catchAsync.js функцію-обгортку, яка прийматиме нашу функцію і повертатиме таку саму функцію, а у разі виникнення помилки передаватиме на обробник помилок нашу еррорку.

Експортуємо цю функцію через хаб index.js

Тепер випробуємо цей код на прикладі нашого кастомного middleware.

Імпортуємо:

Тепер ми огортаємо функцію в catchAsync і прибираємо try{}catch(){}

так само можна переписати всі асинхронні функції в контролерах.

Розгорни, щоб побачити код після рефакторингу

Покликання:

MVC

Last updated