JavaScript и джентльмены: зачем вам паттерны проектирования (и как они упростят жизнь)

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

JavaScript и джентльмены: зачем вам паттерны проектирования (и как они упростят жизнь)

Вы когда-нибудь открывали свой старый проект на JavaScript и думали: «Кто это написал? Это же спагетти-код!»? Если да — добро пожаловать в клуб. Рано или поздно каждый разработчик сталкивается с ситуацией, когда код превращается в хаос. И тут на помощь приходят паттерны проектирования в JavaScript — проверенные рецепты для типовых задач.

Но давайте сразу договоримся: паттерны — это не серебряная пуля. Это не магия, которая сделает ваш код идеальным за одну ночь. Это инструменты. А любой инструмент хорош только тогда, когда вы знаете, когда и зачем его применять. Именно об этом мы и поговорим.

Что такое паттерны проектирования простыми словами?

Представьте, что вы — архитектор. Вы не придумываете каждый раз новую форму колеса или дверного проема. Вы используете проверенные решения: арки, колонны, несущие стены. Паттерны проектирования в JS — это те же «архитектурные чертежи», только для кода.

Они решают три главные проблемы:

  • Повторяемость — не нужно изобретать велосипед;
  • Читаемость — другой разработчик сразу поймет вашу логику;
  • Масштабируемость — код легко расширять без риска все сломать.

В этой статье мы разберем четыре самых популярных паттерна, которые реально пригодятся в повседневной работе. Никакой воды — только код и практика.

1. Singleton (Одиночка): когда вам нужен только один

Бывают ситуации, когда в приложении должен существовать ровно один экземпляр класса. Например:

  • Глобальное хранилище состояния (как Redux store);
  • Логгер для записи ошибок;
  • Подключение к базе данных или API.

Паттерн Singleton гарантирует, что сколько бы раз вы ни пытались создать объект — получите один и тот же.

Пример: Счетчик посещений

class Counter {
  constructor() {
    if (Counter.instance) {
      return Counter.instance;
    }
    this.count = 0;
    Counter.instance = this;
  }

  increment() {
    this.count++;
  }

  getCount() {
    return this.count;
  }
}

// Использование
const counter1 = new Counter();
const counter2 = new Counter();

counter1.increment();
counter1.increment();

console.log(counter2.getCount()); // 2 — потому что это один и тот же объект
console.log(counter1 === counter2); // true

Когда использовать: когда вам критично иметь единую точку доступа к ресурсу. Но будьте осторожны — частый побочный эффект Singleton в JS — глобальное состояние, которое сложно тестировать. Не злоупотребляйте.

2. Module (Модуль): инкапсуляция по-умному

До появления ES6 Modules разработчики использовали паттерн Module для создания приватных и публичных методов. Даже сейчас, когда у нас есть import/export, этот подход остается полезным для организации логики внутри одного файла.

Секрет — в замыканиях (closures). Мы создаем функцию, которая возвращает объект с методами, а внутренние переменные остаются недоступными снаружи.

Пример: Корзина интернет-магазина

const ShoppingCart = (function() {
  // Приватные переменные
  let items = [];
  
  // Приватный метод
  function calculateTotal() {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }

  // Публичный API
  return {
    addItem(product) {
      const existing = items.find(item => item.id === product.id);
      if (existing) {
        existing.quantity++;
      } else {
        items.push({ ...product, quantity: 1 });
      }
    },
    removeItem(productId) {
      items = items.filter(item => item.id !== productId);
    },
    getTotal() {
      return calculateTotal();
    },
    clear() {
      items = [];
    }
  };
})();

// Использование
ShoppingCart.addItem({ id: 1, name: 'Книга', price: 500 });
ShoppingCart.addItem({ id: 1, name: 'Книга', price: 500 });
console.log(ShoppingCart.getTotal()); // 1000
// ShoppingCart.items — undefined (приватная!)

Зачем это нужно: вы скрываете внутреннюю реализацию и защищаете данные от случайного изменения. Это один из базовых js паттернов, который учит дисциплине.

3. Observer (Наблюдатель): всевидящее око

Представьте, что у вас есть система уведомлений. Когда пользователь отправляет форму, нужно обновить три разных компонента: счетчик, список сообщений и звуковое оповещение. Как сделать это элегантно?

Паттерн Observer позволяет одним объектам (субъектам) оповещать множество других (наблюдателей) о своих изменениях. Это основа реактивного программирования.

Пример: Событийная шина для чата

class EventBus {
  constructor() {
    this.listeners = {};
  }

  // Подписаться на событие
  on(event, callback) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(callback);
    // Возвращаем функцию для отписки
    return () => {
      this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
    };
  }

  // Вызвать событие
  emit(event, data) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(callback => callback(data));
    }
  }
}

// Использование
const bus = new EventBus();

// Подписчики
const unsubscribe = bus.on('newMessage', (msg) => {
  console.log(`Новое сообщение: ${msg.text}`);
});

bus.on('newMessage', (msg) => {
  updateBadge(msg.chatId);
});

// Где-то в коде
bus.emit('newMessage', { text: 'Привет!', chatId: 123 });

// Отписка
unsubscribe();

Когда использовать: когда у вас есть связь «один ко многим» и нужно избежать жесткой связанности. Observer javascript часто встречается в фреймворках (например, Vue.js использует его для реактивности).

4. Factory Method (Фабричный метод): создание без головной боли

Бывает, что тип создаваемого объекта зависит от входных данных. Например, вы пишете UI-библиотеку, где кнопки, поля ввода и чекбоксы — это разные классы, но создаются они через один интерфейс.

Factory Method выносит логику создания объектов в отдельный метод. Это избавляет от кучи условий if/else и делает код гибким.

Пример: Генератор элементов формы

// Разные типы полей
class TextInput {
  render() { return '<input type="text" />'; }
}

class Checkbox {
  render() { return '<input type="checkbox" />'; }
}

class Button {
  render() { return '<button>Click</button>'; }
}

// Фабрика
class FormElementFactory {
  static createElement(type) {
    switch(type) {
      case 'text':
        return new TextInput();
      case 'checkbox':                

Блоги

Рекомендаций по книгам