Паттерны проектирования в Node.js: какие реально нужны
Содержание
О проверке кода. Примеры не запускались — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».
Паттерны проектирования пришли из мира Java и C++, где язык жёсткий, а обходных путей мало. В JavaScript половина из них либо встроена, либо не нужна. Разберём, что реально применяют в Node, а что можно забыть.
Синглтон: в Node он бесплатный
Классический синглтон — объект, существующий в единственном экземпляре. В Java ради этого пишут приватный конструктор и статический метод. В Node этого делать не нужно вообще.
// db.js
import { createPool } from 'mysql2/promise';
const pool = createPool({ /* ... */ });
export default pool; // экспортируем ЭКЗЕМПЛЯР
// в любом другом файле
import pool from './db.js'; // всегда ОДИН И ТОТ ЖЕ pool
Модуль в Node кэшируется. Первый import выполняет код файла, все последующие получают уже готовый результат из кэша. Экспортировали экземпляр — получили синглтон, без единой строки паттерна.
Именно так делают пул соединений к базе, клиент Redis, конфиг: один объект на всё приложение, разделяемый через импорт.
Наблюдатель: это EventEmitter
Паттерн «наблюдатель» — объект уведомляет подписчиков об изменениях. В Node он не паттерн, а встроенный класс:
import { EventEmitter } from 'node:events';
class OrderService extends EventEmitter {
createOrder(data) {
// ...создали заказ...
this.emit('order:created', data); // уведомили всех подписчиков
}
}
const orders = new OrderService();
orders.on('order:created', (o) => sendEmail(o));
orders.on('order:created', (o) => logAnalytics(o));
Это буквально Observer из учебника — подробно разобранный отдельно. Реализовывать его руками бессмысленно: платформа уже дала.
Что из классики встроено в язык
Уже в языке — не пишите
Встроено, отдельный паттерн не нужен
- Синглтон → кэш модулей
- Наблюдатель →
EventEmitter - Итератор → протокол
for..of - Стратегия → функция как аргумент
- Декоратор → функции высшего порядка
Реально полезны в Node
Решают настоящие задачи
- Фабрика — создание по условию
- Фасад — обёртка над сложным API
- Middleware — цепочка обработчиков
- Адаптер — приведение чужого интерфейса
Стратегия в JavaScript — это просто передача функции:
// не нужен класс Strategy — достаточно функции
function sort(arr, compareFn) {
return [...arr].sort(compareFn);
}
sort(users, (a, b) => a.age - b.age); // одна стратегия
sort(users, (a, b) => a.name.localeCompare(b.name)); // другая
В Java ради этого пишут интерфейс и три класса. В JavaScript функция — объект первого класса, и вся конструкция схлопывается в один аргумент.
Что действительно применяют: фабрика
Фабрика прячет создание объекта за функцией — полезно, когда тип зависит от данных:
function createLogger(env) {
if (env === 'production') return new FileLogger();
if (env === 'test') return new SilentLogger();
return new ConsoleLogger();
}
const logger = createLogger(process.env.NODE_ENV);
Вызывающий код не знает и не должен знать, какой именно логгер он получил. Смените реализацию внутри фабрики — остальной код не заметит. Это настоящая польза, а не церемония.
Фасад: обёртка над сложностью
Фасад прячет громоздкий API за простым интерфейсом:
// вместо десяти вызовов чужой библиотеки — один понятный метод
class PaymentGateway {
async charge(amount, card) {
const token = await this.provider.tokenize(card);
const session = await this.provider.createSession(token);
return this.provider.capture(session.id, amount);
}
}
Снаружи — gateway.charge(100, card). Внутри — три шага чужого SDK. Когда SDK сменится, поправите один класс, а не весь проект.
Главное правило: паттерн — ответ, а не цель
Самая частая ошибка с паттернами — применять их, потому что знаешь, а не потому что есть проблема.
Паттерн — это решение конкретной задачи. Нет задачи — паттерн только усложняет код. Фабрика ради одного типа объекта, синглтон там, где хватило бы переменной, наблюдатель на два обработчика — всё это добавляет слоёв, не решая ничего.
Правильный порядок: сначала проблема, потом паттерн. Увидели, что тип объекта зависит от условий и это повторяется — вот тогда фабрика. Заранее «спроектировать красиво» обычно означает усложнить.
Это та же мысль, что проходит через весь сайт: ORM не нужен для простого запроса, Redux не нужен для локального состояния, фреймворк не нужен для маленького API. Инструмент берут под задачу, а не наоборот.
Что было на этой странице в 2017 году
Первая версия вышла 18 августа 2017 года и разбирала асинхронные паттерны Node — управление потоком выполнения через колбэки. Для 2017 года это была живая боль: промисов в широком ходу ещё не было, и «паттерны» асинхронности означали способы не утонуть в колбэк-аде.
Что изменилось радикально:
- Асинхронные паттерны 2017 года устарели целиком. Управление потоком через вложенные колбэки, библиотеки вроде
async.js— всё это заменилasync/await. То, что было паттерном, стало синтаксисом языка. - Отношение к классическим паттернам сместилось. Стало понятнее, что половина GoF-паттернов в JavaScript не нужна — язык гибче Java, и многое встроено.
Поэтому мы переписали урок не про «как бороться с асинхронностью» — эта проблема решена, — а про то, какие паттерны в Node реально нужны, а какие можно забыть. Это полезнее: асинхронность больше не болит, а вопрос «нужен ли здесь паттерн» встаёт каждый день.
Смежные темы: EventEmitter — паттерн наблюдатель встроенным; middleware — цепочка обработчиков; промисы — что заменило асинхронные паттерны. Полный список — в справочнике по Node.js.