Паттерны проектирования в 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 из учебника — подробно разобранный отдельно. Реализовывать его руками бессмысленно: платформа уже дала.

Что из классики встроено в язык

Классические паттерны в JavaScript

Уже в языке — не пишите

Встроено, отдельный паттерн не нужен

  • Синглтон → кэш модулей
  • Наблюдатель → 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.

Частые вопросы

Нужны ли классические паттерны GoF в JavaScript?
Часть — да, часть встроена в язык и не нужна как отдельный паттерн. Синглтон в Node — это просто модуль. Наблюдатель — это EventEmitter. Итератор — это протокол for..of. Заучивать их как в Java нет смысла.
Как сделать синглтон в Node.js?
Никак специально: модуль в Node кэшируется и по факту уже синглтон. Первый импорт выполняет код модуля, все последующие получают тот же объект. Экспортируйте экземпляр — и это готовый синглтон.
Что такое паттерн «наблюдатель» в Node?
Это EventEmitter. Одна часть кода эмитит события, другие подписаны — классический Observer, встроенный в платформу. Отдельно его реализовывать не надо.
Чем фабрика лучше прямого new?
Фабрика прячет, какой именно объект создаётся, за одной функцией. Полезна, когда тип зависит от входных данных или когда создание сложное. Для простого объекта прямой литерал понятнее.
Стоит ли увлекаться паттернами?
Нет. Паттерн — ответ на конкретную проблему, а не украшение. Применённый без нужды, он усложняет код. Сначала проблема, потом паттерн — не наоборот.