Гид по технологиям

Паттерны проектирования в JavaScript

6 min read Программирование Обновлено 04 Jan 2026
Паттерны JavaScript: модуль и наблюдатель
Паттерны JavaScript: модуль и наблюдатель

Буквенные блоки, образующие слово «JAVASCRIPT»

Введение

Паттерны проектирования — это проверенные решения для распространённых проблем разработки. В контексте JavaScript они помогают организовать код, контролировать область видимости, уменьшить связанность компонентов и упростить тестирование.

Определение термина: паттерн проектирования — повторяемое архитектурное или кодовое решение для общей задачи, не привязанное к конкретной реализации.

Важно понимать абстракции за паттернами: знание примера полезно, но критически важно понимать, почему паттерн работает и когда он неприменим.

Модульный паттерн

Модульный паттерн (Module pattern) обеспечивает инкапсуляцию: скрывает приватные данные и предоставляет явный публичный API. В JavaScript его часто реализуют через замыкания (IIFE) или через современные ES-модули.

Ключевая идея: создать локальную область видимости для приватных переменных и возвратить объект с методами, которые оперируют этой областью.

Пример на основе IIFE (оригинальная форма):

const ShoppingCartModule = (function () {  
  // Private data  
  let cartItems = [];  
  
  // Private method  
  function calculateTotalItems() {  
    return cartItems.reduce((total, item) => total + item.quantity, 0);  
  }  
  
  // Public API  
  return {  
    addItem(item) {  
      cartItems.push(item);  
    },  
  
    getTotalItems() {  
      return calculateTotalItems();  
    },  
  
    clearCart() {  
      cartItems = [];  
    }  
  };  
})();  
  
// Usage example  
ShoppingCartModule.addItem({ name: 'Product 1', quantity: 2 });  
ShoppingCartModule.addItem({ name: 'Product 2', quantity: 1 });  
  
console.log(ShoppingCartModule.getTotalItems()); // Output: 3  
  
ShoppingCartModule.clearCart();  
console.log(ShoppingCartModule.getTotalItems()); // Output: 0  

Пояснения:

  • IIFE создаёт приватную область видимости, где определены cartItems и calculateTotalItems.
  • Внешняя область видимости не имеет прямого доступа к cartItems.
  • Возвращаемый объект служит публичным API.

Современная альтернатива: ES-модули

ES-модули (import/export) дают нативную инкапсуляцию на уровне файла и удобны для сборки и деревьев зависимостей.

Пример ES-модуля (shoppingCart.js):

let cartItems = [];

export function addItem(item) {
  cartItems.push(item);
}

export function getTotalItems() {
  return cartItems.reduce((total, item) => total + item.quantity, 0);
}

export function clearCart() {
  cartItems = [];
}

В другом файле:

import { addItem, getTotalItems, clearCart } from './shoppingCart.js';

addItem({ name: 'Product 1', quantity: 2 });
console.log(getTotalItems());

Преимущества ES-модулей:

  • Явная система зависимостей.
  • Поддержка статического анализа и tree-shaking.
  • Простая интеграция с современными сборщиками и TypeScript.

Когда модульный паттерн не подходит

  • Малые одноразовые утилиты: добавление модульного слоя может быть избыточным.
  • Высокочастотные объекты со сложной сериализацией состояния: скрытие состояния усложняет отладку и сериализацию.

Советы по безопасности и тестированию

Важно: приватные данные хорошо защищены от случайного изменения, но это не замена политике безопасности. Для тестирования приватных функций используйте тестирование через публичный API или рефакторинг в сервисы с явными интерфейсами.

Паттерн наблюдатель

Паттерн наблюдатель (Observer) устанавливает связь «один-ко-многим»: объект (субъект) при изменении состояния уведомляет подписчиков. Он помогает реализовать реактивность и слабую связанность между компонентами.

Пример классической реализации (функциональный стиль):

function NotificationSystem() {  
  this.subscribers = [];  
  this.subscribe = function (subscriber) {  
    this.subscribers.push(subscriber);  
  };  
  this.unsubscribe = function (subscriber) {  
    const index = this.subscribers.indexOf(subscriber);  
    if (index !== -1) {  
      this.subscribers.splice(index, 1);  
    }  
  };  
  this.notify = function (message) {  
    this.subscribers.forEach(function (subscriber) {  
      subscriber.receiveNotification(message);  
    });  
  };  
}  

function Subscriber(name) {  
  this.receiveNotification = function (message) {  
    console.log(name + ' received notification: ' + message);  
  };  
}  

const notificationSystem = new NotificationSystem();  
const subscriber1 = new Subscriber('Subscriber 1');  
const subscriber2 = new Subscriber('Subscriber 2');  
notificationSystem.subscribe(subscriber1);  
notificationSystem.subscribe(subscriber2);  
notificationSystem.notify('New notification!');  

Современные API для наблюдателя

  • DOM/EventTarget и CustomEvent удобны для веб-ориентированных приложений.
  • RxJS и подобные библиотеки дают мощные операторы для работы с потоками событий.

Пример с EventTarget и CustomEvent:

const bus = new EventTarget();

function subscribe(handler) {
  bus.addEventListener('cart:updated', (e) => handler(e.detail));
}

function publishUpdate(detail) {
  bus.dispatchEvent(new CustomEvent('cart:updated', { detail }));
}

subscribe((data) => console.log('Получено', data));
publishUpdate({ totalItems: 3 });

Когда наблюдатель не подходит

  • Ненужное рассылание частых мелких событий может создать узкие места по производительности.
  • Жёстко связанная логика публикации и подписки (например, когда важно гарантированно обработать событие синхронно) требует других подходов.

Как выбирать между паттернами

Выбор паттерна зависит от потребностей:

  • Если нужно изолировать состояние и предоставить ограниченный API — модуль.
  • Если нужно асинхронно уведомлять множество слушателей — наблюдатель.
  • Если нужны сложные реактивные трансформации — рассмотреть RxJS (или аналог).

Ниже — упрощённая диаграмма принятия решения.

flowchart TD
  A[Нужно ли изолировать состояние?] -->|Да| B[Использовать модуль]
  A -->|Нет| C[Нужно ли уведомлять нескольких подписчиков?]
  C -->|Да| D[Использовать наблюдатель или EventBus]
  C -->|Нет| E[Простая функция/утилита]
  D --> F{Требуется сложная трансформация потоков?}
  F -->|Да| G[Использовать RxJS]
  F -->|Нет| H[Использовать EventTarget/CustomEvent]

Ментальные модели и эмпирические эвристики

  • «Ограда вокруг состояния»: если вы хотите строить «стены» вокруг данных, думайте о модулях.
  • «Издатель и слушатель»: если событие должно распространяться — применяйте наблюдатель.
  • Evite premature optimization: начинайте с простого подхода и рефакторьте, если появляются проблемы с масштабом.

Правило 80/20: большинство случаев покрывается простыми модульными структурами и нативными событиями; библиотеки и сложные паттерны нужны для 20% сложных случаев.

Анти-паттерны и распространённые ошибки

  • Создание глобального EventBus без явной контрактности сообщений ведёт к трудноотлаживаемому коду.
  • Чрезмерная инкапсуляция (скрытие всего подряд) усложняет интеграцию и тестирование.
  • Использование промышленных паттернов там, где достаточно простой функции.

Важно: избегайте «magic strings» при использовании событий — централизуйте имена событий и форматы payload.

Тесты и критерии приёмки

Критерии приёмки для модуля:

  • Публичные методы покрыты unit-тестами.
  • Приватные данные недоступны напрямую извне (проверяется через API).
  • Поведение на граничных значениях задокументировано.

Критерии приёмки для системы уведомлений:

  • Подписчики получают сообщения в ожидаемом формате.
  • При отписке — подписчик больше не получает уведомления.
  • Система корректно обрабатывает ошибки в обработчиках (один упавший подписчик не ломает остальных).

Пример тест-кейса (псевдо):

  • Arrange: создать NotificationSystem и двух подписчиков.
  • Act: отправить уведомление.
  • Assert: оба подписчика получили сообщение.

Рольовые чек-листы

Для разработчика:

  • Определить областность данных (локальная/глобальная).
  • Решить, нужен ли публичный API или набор функций.
  • Добавить тесты на публичные методы.

Для архитектора:

  • Оценить масштабируемость и производительность паттерна.
  • Проверить совместимость с остальной системой (bundle size, tree-shaking).

Для ревьювера кода:

  • Убедиться, что приватное состояние не утекло наружу.
  • Проверить обработку ошибок и логи.

Мaturity levels — уровни зрелости реализации

  • Начальный: простые функции и локальные объекты (пока нет потребности в масштабировании).
  • Средний: модульная структура и централизованные события (EventTarget), тесты и документация.
  • Продвинутый: использование библиотек (RxJS), соглашений по событиям, метрик производительности и автоматических тестов нагрузки.

Факто-бокс: что учитывать

  • ES-модули работают в большинстве современных сред (Node.js >= 12+ с флагами/настройками и в браузерах).
  • EventTarget/CustomEvent поддерживается в браузерах; для Node.js используйте EventEmitter или полифил.
  • RxJS добавляет мощь, но увеличивает размер бандла.

Безопасность и приватность

  • Инкапсуляция не защищает от злонамеренного кода в одной странице/процессе. Для реальной безопасности используйте контекстные изоляции (iframe, отдельный worker, сервер).
  • Если вы храните чувствительные данные в модулях, следите за сериализацией и логированием: не выводите секреты в логи.

Шаблоны и сниппеты (cheat sheet)

Revealing Module (показывает только нужные методы):

const Counter = (function () {
  let count = 0;
  function increment() { count += 1; }
  function get() { return count; }
  return { increment, get };
})();

Event bus на базе EventTarget:

class Bus extends EventTarget {}
const bus = new Bus();

bus.addEventListener('user:login', (e) => console.log('user', e.detail));

bus.dispatchEvent(new CustomEvent('user:login', { detail: { id: 1 } }));

Тесты производительности и под нагрузкой

Примечание: массовая рассылка синхронных уведомлений может блокировать основной поток. Для интенсивных потоков событий используйте:

  • батчинг событий, debouncing/throttling;
  • Web Workers для фоновой обработки;
  • специализированные очереди и backpressure (в RxJS или через native streams).

Совместимость и миграция

  • Миграция с IIFE на ES-модули: экспортируйте публичные методы и обновите импорты.
  • Переход от собственного EventBus к EventTarget/CustomEvent: смена API может потребовать обновления имён событий и формата payload.

Примеры, где паттерны дают сбой

  • В realtime-играх глобальный EventBus может стать узким местом — требуется распределённая архитектура.
  • В очень простых скриптах модульность приносит лишнюю абстракцию.

Глоссарий (1 строка каждое)

  • Модуль: единица кода с приватным состоянием и публичным интерфейсом.
  • Наблюдатель: компонент, подписывающийся на события субъекта.
  • IIFE: немедленно вызываемая функция для создания локальной области видимости.
  • ES-модуль: современный способ организации кода с import/export.

Заключение

Паттерны модулей и наблюдателя — фундаментальные инструменты для организации JavaScript-кода. Применяйте их разумно: начните с нативных возможностей языка (ES-модули, EventTarget), тестируйте публичный API, избегайте ненужной сложности и следите за производительностью. Регулярная рефакторизация и код-ревью помогут поддерживать кодовую базу в хорошем состоянии.

Важно: паттерн — не догма. Подходите к выбору прагматично и адаптируйте решения под реальные требования.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

RDP: полный гид по настройке и безопасности
Инфраструктура

RDP: полный гид по настройке и безопасности

Android как клавиатура и трекпад для Windows
Гайды

Android как клавиатура и трекпад для Windows

Советы и приёмы для работы с PDF
Документы

Советы и приёмы для работы с PDF

Calibration в Lightroom Classic: как и когда использовать
Фото

Calibration в Lightroom Classic: как и когда использовать

Отключить Siri Suggestions на iPhone
iOS

Отключить Siri Suggestions на iPhone

Рисование таблиц в Microsoft Word — руководство
Office

Рисование таблиц в Microsoft Word — руководство