Паттерны проектирования в JavaScript: какие полезны и когда они лишние
Паттерны проектирования в современном JavaScript и TypeScript: модуль, фабрика, адаптер, фасад, декоратор, стратегия, наблюдатель, команда, конечный автомат, паттерны React на хуках и признаки неуместного паттерна.
Паттерны проектирования — это не набор классов, которые нужно применить в проекте, а словарь для типовых решений типовых проблем. Когда разработчик говорит «здесь стратегия» или «сделаем адаптер к платёжному провайдеру», команда сразу понимает, о какой структуре кода идёт речь. Проблемы начинаются, когда паттерны применяют ради паттернов: фабрика фабрик для создания одного объекта, синглтон вместо обычного модуля, наблюдатель там, где хватило бы вызова функции. JavaScript добавляет свою специфику. Многие классические паттерны из книги «банды четырёх» писались для языков без функций первого класса и модулей, и в JavaScript часть из них выражается гораздо проще — функцией, замыканием или импортом. Ниже — паттерны, которые действительно полезны в современном JavaScript и TypeScript, с примерами из прикладных задач, и признаки того, что паттерн применён не к месту. Зачем нужны паттерны Общий язык команды. Название паттерна заменяет долгое описание структуры кода на ревью и в документации. Проверенные решения. Типовые проблемы — выбор алгоритма во время работы, реакция на события, подключение внешних систем — уже решены, и у решений известны сильные и слабые стороны. Гибкость в местах изменений. Паттерн полезен там, где требования действительно меняются: добавляются способы оплаты, каналы уведомлений, форматы выгрузки. Правило, которое стоит держать в голове: паттерн добавляет косвенность. Если место в коде не меняется и не будет меняться, косвенность только усложняет чтение. Порождающие паттерны Модуль и синглтон В классических языках синглтон гарантирует единственный экземпляр класса. В JavaScript модуль выполняется один раз и кэшируется, поэтому экспортированный объект уже является единственным экземпляром. // db.js — единственный пул подключений на приложение import pg from 'pg'; export const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); export function query(text, params) { return pool.query(text, params); } Осторожно: глобальное состояние в модуле затрудняет тестирование и может неожиданно разделяться между запросами на сервере. Для серверного рендеринга особенно опасно хранить в модуле данные конкретного пользователя. Фабрика Функция, которая создаёт объекты нужного вида в зависимости от параметров и скрывает детали создания. function createNotifier(channel, config) { switch (channel) { case 'email': return new EmailNotifier(config.smtp); case 'sms': return new SmsNotifier(config.sms); case 'telegram': return new TelegramNotifier(config.telegram); default: throw new Error(`Неизвестный канал: ${channel}`); } } Строитель Пошаговое создание сложного объекта с множеством необязательных параметров. В JavaScript его часто заменяет объект параметров, но строитель полезен, когда шаги имеют порядок и проверки — например, при сборке запросов или отчётов. const report = new ReportBuilder() .period('2026-01-01', '2026-03-31') .groupBy('manager') .include('returns') .format('xlsx') .build(); // проверка обязательных параметров здесь Структурные паттерны Адаптер Приводит интерфейс внешней системы к интерфейсу, который ожидает приложение. Один из самых полезных паттернов в прикладной разработке: платёжные провайдеры, службы доставки, CRM и сервисы рассылок имеют разные API, а бизнес-логика работает с единым интерфейсом. class SbpPaymentAdapter { constructor(client) { this.client = client; } async charge({ amount, orderId }) { const res = await this.client.createQr({ sum: amount * 100, externalId: orderId }); return { paymentId: res.qrcId, status: mapStatus(res.state), payUrl: res.payload }; } } class CardPaymentAdapter { constructor(client) { this.client = client; } async charge({ amount, orderId }) { const res = await this.client.payments.create({ amount, order: orderId }); return { paymentId: res.id, status: mapStatus(res.status), payUrl: res.confirmationUrl }; } } Смена провайдера или добавление нового затрагивает только адаптер. Именно такая изоляция внешних зависимостей упрощает импортозамещение сервисов — об этом статья об импортозамещении digital-стека . Фасад Простой интерфейс к сложной подсистеме. Вместо того чтобы вызывающий код знал о резервировании на складе, расчёте доставки, создании платежа и отправке уведомления, он вызывает одну функцию оформления заказа. Декоратор Добавляет поведение, не меняя исходный объект или функцию. В JavaScript естественно реализуется функциями высшего порядка. function withRetry(fn, { attempts = 3, delayMs = 500 } = {}) { return async (...args) = { for (let i = 1; ; i++) { try { return await fn(...args); } catch (error) { if (i = attempts) throw error; await new Promise((r) = setTimeout(r, delayMs * i)); } } }; } const fetchRatesWithRetry = withRetry(fetchExchangeRates, { attempts: 5 }); Так же добавляются кэширование, журналирование, измерение времени и ограничение частоты вызовов. Прокси Объект-посредник, контролирующий доступ к другому объекту. Встроенный Proxy в JavaScript позволяет перехватывать чтение и запись свойств — на этом построена реактивность ряда фреймворков, а в прикладном коде полезен для валидации, ленивой загрузки и защиты от записи. const readonlyConfig = new Proxy(config, { set() { throw new Error('Конфигурация доступна только для чтения'); }, }); Поведенческие паттерны Стратегия Семейство взаимозаменяемых алгоритмов, выбираемых во время работы. В JavaScript стратегия часто — просто объект с функциями. const pricingStrategies = { retail: (price) = price, wholesale: (price, qty) = (qty = 100 ? price * 0.85 : price * 0.93), partner: (price) = price * 0.8, }; function calculatePrice(client, price, qty) { const strategy = pricingStrategies[client.priceType] ?? pricingStrategies.retail; return strategy(price, qty); } Стратегия заменяет растущие цепочки условий: новое правило добавляется новой записью, а не новой веткой в функции, которую боятся трогать. Наблюдатель Объект уведомляет подписчиков о событиях, не зная, кто они. В JavaScript это EventEmitter в Node.js, EventTarget в браузере и подписки в библиотеках состояния. import { EventEmitter } from 'node:events'; export const orderEvents = new EventEmitter(); // оформление заказа не знает, кто подписан orderEvents.emit('order:paid', { orderId, clientId, total }); // подписчики в своих модулях orderEvents.on('order:paid', sendReceipt); orderEvents.on('order:paid', syncToCrm); Ограничения: трудно проследить, что происходит после события, ошибка в подписчике может остаться незамеченной, а события внутри процесса теряются при его перезапуске. Для надёжной обработки между сервисами используют брокеры сообщений — подробно в статье об event-driven архитектуре . Команда Действие упаковывается в объект с методами выполнения и отмены. Основа для отмены и повтора действий в редакторах, очередей операций и журнала изменений. const moveBlock = (editor, id, to) = { const from = editor.positionOf(id); return { execute: () = editor.move(id, to), undo: () = editor.move(id, from), }; }; Цепочка обязанностей Запрос проходит через последовательность обработчиков, каждый из которых может обработать его или передать дальше. Промежуточные обработчики в Express, Koa и других серверных фреймворках — именно этот паттерн: аутентификация, проверка прав, валидация, журналирование. Конечный автомат Не входит в классический список «банды четырёх», но в прикладной разработке один из самых полезных. Сущность находится в одном из явно описанных состояний, и переходы разрешены только по определённым событиям. const orderTransitions = { new: { pay: 'paid', cancel: 'cancelled' }, paid: { ship: 'shipped', refund: 'refunded' }, shipped: { deliver: 'delivered' }, delivered: {}, cancelled: {}, refunded: {}, }; function transition(order, event) { const next = orderTransitions[order.status]?.[event]; if (!next) throw new Error(`Переход «${event}» невозможен из «${order.status}»`); return { ...order, status: next }; } Автомат не позволяет отгрузить неоплаченный заказ или вернуть деньги дважды — ошибки, которые в коде с набором булевых флагов возникают постоянно. Паттерны в React Часть паттернов, популярных несколько лет назад, в современном React вытеснены хуками. Компоненты высшего порядка и render props раньше использовались для переиспользования логики. Сегодня для этого обычно пишут пользовательские хуки: они проще читаются и не создают лишней вложенности компонентов. Пользовательские хуки — основной способ выделить и переиспользовать логику с состоянием: загрузку данных, подписки, работу с формами. Составные компоненты — набор связанных компонентов, разделяющих состояние через контекст: вкладки, выпадающие меню, аккордеоны. Дают гибкую разметку без десятков свойств. Контейнер и представление в строгом виде уже не обязательны, но идея разделения логики и отображения остаётся полезной — теперь её реализуют хуки. // Пользовательский хук вместо компонента высшего порядка function useOnlineStatus() { const [online, setOnline] = useState(navigator.onLine); useEffect(() = { const update = () = setOnline(navigator.onLine); window.addEventListener('online', update); window.addEventListener('offline', update); return () = { window.removeEventListener('online', update); window.removeEventListener('offline', update); }; }, []); return online; } Об управлении состоянием и выборе инструментов — в статье об управлении состоянием в React . Признаки неуместного паттерна у интерфейса одна реализация, и вторая не планируется; чтобы понять простое действие, нужно пройти через фабрику, стратегию и адаптер; класс создан там, где хватило бы функции; события используются вместо прямого вызова внутри одного модуля, и проследить поток выполнения невозможно; название паттерна в имени класса есть, а решаемой им проблемы нет. Практичный подход: писать простой код и вводить паттерн, когда появилась вторая-третья реализация или когда изменение в одном месте начинает требовать правок во многих. Рефакторинг к паттерну по факту надёжнее, чем проектирование паттернов заранее. Принципы, которые помогают принимать такие решения, — в статье о чистом коде . Архитектуру приложений с понятной структурой и изолированными интеграциями мы закладываем в проектах разработки веб-проектов и автоматизации . Частые вопросы Нужно ли знать все паттерны «банды четырёх»? Полезно знать названия и идеи, чтобы понимать коллег и документацию. Активно в прикладной разработке на JavaScript используется меньшая часть: адаптер, фасад, декоратор, стратегия, наблюдатель, команда и конечный автомат. Синглтон — антипаттерн? Сам по себе нет, но глобальное изменяемое состояние затрудняет тестирование и создаёт скрытые зависимости. В JavaScript модуль уже даёт единственный экземпляр; важнее, чтобы в нём не хранилось состояние конкретного запроса или пользователя. Паттерны нужны только в объектно-ориентированном коде? Нет. Многие паттерны в функциональном стиле выражаются проще: стратегия — объект с функциями, декоратор — функция высшего порядка, команда — пара функций. Идея важнее формы. Как паттерны связаны с архитектурой? Паттерны проектирования решают задачи на уровне модулей и классов. Архитектурные паттерны — слоистая архитектура, микросервисы, событийная архитектура — решают задачи на уровне системы. Адаптеры к внешним системам — хороший пример паттерна, который напрямую поддерживает архитектурное решение изолировать внешние зависимости. Помогает ли TypeScript применять паттерны? Да: интерфейсы делают контракты стратегий и адаптеров явными, а размеченные объединения и проверка полноты естественно подходят для конечных автоматов. Подробно — в статье о продвинутом TypeScript . С чего начать внедрение паттернов в существующий код? С мест, которые чаще всего меняются и доставляют больше всего проблем: интеграций с внешними сервисами — адаптеры, растущих цепочек условий — стратегии, сущностей со сложными статусами — конечные автоматы.