← Курс/Паттерны проектирования в JavaScript#138 из 257+40 XP

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

Краткий ответ

Паттерны проектирования — это проверенные решения типичных задач. В JS наиболее востребованы: Observer/EventEmitter (подписки на события), Singleton (один экземпляр), Factory (фабрика объектов), Decorator (расширение поведения), Strategy (замена алгоритмов), Module (инкапсуляция через замыкания). JS позволяет реализовывать паттерны лаконично благодаря замыканиям и прототипному наследованию.

Полный разбор

Observer / EventEmitter — поведенческий

Один из самых важных паттернов в JS. Позволяет объектам подписываться на события и реагировать на изменения.

class EventEmitter {
  constructor() {
    this._events = {}
  }

  // Подписка на событие
  on(event, listener) {
    if (!this._events[event]) {
      this._events[event] = []
    }
    this._events[event].push(listener)
    return this  // для цепочки вызовов
  }

  // Отписка от события
  off(event, listener) {
    if (!this._events[event]) return this
    this._events[event] = this._events[event].filter(l => l !== listener)
    return this
  }

  // Подписка только на одно срабатывание
  once(event, listener) {
    const wrapper = (...args) => {
      listener(...args)
      this.off(event, wrapper)
    }
    return this.on(event, wrapper)
  }

  // Генерация события
  emit(event, ...args) {
    if (!this._events[event]) return false
    this._events[event].forEach(listener => listener(...args))
    return true
  }
}

// Использование
const emitter = new EventEmitter()
emitter.on('data', (payload) => console.log('Получено:', payload))
emitter.emit('data', { id: 1, value: 42 })  // 'Получено: {id: 1, value: 42}'

Singleton — порождающий

Гарантирует единственный экземпляр класса. В JS реализуется через замыкание или статическое свойство.

// Через замыкание (модульный паттерн)
const AppConfig = (() => {
  let instance = null

  function createInstance() {
    return {
      theme: 'light',
      language: 'ru',
      apiUrl: 'https://api.example.com'
    }
  }

  return {
    getInstance() {
      if (!instance) {
        instance = createInstance()
      }
      return instance
    }
  }
})()

const config1 = AppConfig.getInstance()
const config2 = AppConfig.getInstance()
console.log(config1 === config2)  // true — один и тот же объект

// Через класс со статическим свойством
class Database {
  static #instance = null

  constructor(url) {
    if (Database.#instance) {
      return Database.#instance
    }
    this.url = url
    this.connected = false
    Database.#instance = this
  }

  static getInstance(url) {
    if (!Database.#instance) {
      new Database(url)
    }
    return Database.#instance
  }
}

Factory — порождающий

Создаёт объекты без указания их конкретного класса. Инкапсулирует логику создания.

// Простая фабрика
function createUser(type, name) {
  const base = {
    name,
    createdAt: new Date().toISOString()
  }

  const roles = {
    admin:   { ...base, permissions: ['read', 'write', 'delete'], role: 'admin' },
    editor:  { ...base, permissions: ['read', 'write'], role: 'editor' },
    viewer:  { ...base, permissions: ['read'], role: 'viewer' }
  }

  if (!roles[type]) throw new Error('Unknown user type: ' + type)
  return roles[type]
}

const admin  = createUser('admin', 'Алиса')
const viewer = createUser('viewer', 'Боб')

Module — структурный

Инкапсулирует состояние и поведение, предоставляет публичный API через замыкание.

const Counter = (() => {
  let count = 0  // приватное состояние

  return {
    increment() { count++ },
    decrement() { count-- },
    reset()     { count = 0 },
    getValue()  { return count }
    // count недоступен напрямую извне!
  }
})()

Counter.increment()
Counter.increment()
Counter.getValue()  // 2
// Counter.count    // undefined — инкапсулировано

Decorator — структурный

Добавляет поведение объекту/функции без изменения оригинала.

// Декоратор функции — добавляет логирование
function withLogging(fn, name = fn.name) {
  return function(...args) {
    console.log(`[${name}] вызов с:'`, args)
    const result = fn.apply(this, args)
    console.log(`[${name}] результат:'`, result)
    return result
  }
}

const add = (a, b) => a + b
const loggedAdd = withLogging(add)
loggedAdd(2, 3)  // [add] вызов с: [2, 3]; [add] результат: 5

Strategy — поведенческий

Определяет семейство алгоритмов, инкапсулирует каждый, делает их взаимозаменяемыми.

// Сортировка с разными стратегиями
const sortStrategies = {
  bubble(arr) {
    const a = [...arr]
    for (let i = 0; i < a.length - 1; i++) {
      for (let j = 0; j < a.length - i - 1; j++) {
        if (a[j] > a[j + 1]) [a[j], a[j + 1]] = [a[j + 1], a[j]]
      }
    }
    return a
  },
  quick(arr) {
    if (arr.length <= 1) return arr
    const pivot = arr[Math.floor(arr.length / 2)]
    const left  = arr.filter(x => x < pivot)
    const mid   = arr.filter(x => x === pivot)
    const right = arr.filter(x => x > pivot)
    return [...sortStrategies.quick(left), ...mid, ...sortStrategies.quick(right)]
  }
}

class Sorter {
  constructor(strategy = 'quick') {
    this.strategy = sortStrategies[strategy]
  }
  setStrategy(name) { this.strategy = sortStrategies[name] }
  sort(arr) { return this.strategy(arr) }
}

Антипаттерны

  • **God Object** — один объект знает и делает всё (нарушает принцип единственной ответственности)
  • **Spaghetti Code** — запутанный поток управления без чёткой структуры
  • **Magic Numbers** — числа без именованных констант (вместо 86400 должна быть const SECONDS_PER_DAY = 86400)
  • Связанные уроки курса

  • Замыкания — основа Module и Singleton паттернов в JS
  • События — Observer паттерн встроен в DOM (addEventListener)
  • Классы — Singleton, Factory и Command удобно реализовывать через классы
  • Как отвечать на собеседовании

    Не нужно перечислять все 23 паттерна GoF. Сосредоточься на трёх-четырёх, которые реально использовал: Observer, Singleton, Factory, Decorator. Для каждого: назови проблему → покажи паттерн → объясни преимущества. Упомяни конкретный пример из реального кода (EventEmitter, кэширование через Singleton).

    Красные флаги ответа

  • «Паттерны — это отдельная тема, я их не учил» — паттерны это повседневный код, EventEmitter есть в каждом Node.js приложении
  • Рассказ о паттернах без кода — интервьюер ждёт, что ты напишешь реализацию на доске/экране
  • Путаница Observer и Pub/Sub — Observer: подписчики знают об источнике; Pub/Sub: через брокера, стороны не знают друг о друге
  • Примеры

    Реализация EventEmitter, Singleton через замыкание, Factory, и практический пример их совместного использования

    // ===== EVENT EMITTER (Observer) =====
    console.log('=== EventEmitter ===')
    
    class EventEmitter {
      constructor() {
        this._events = {}
        this._onceWrappers = new WeakMap()  // для корректной отписки once
      }
    
      on(event, listener) {
        (this._events[event] ??= []).push(listener)
        return this
      }
    
      off(event, listener) {
        if (!this._events[event]) return this
        this._events[event] = this._events[event].filter(l => l !== listener)
        return this
      }
    
      once(event, listener) {
        const wrapper = (...args) => {
          listener(...args)
          this.off(event, wrapper)
        }
        this._onceWrappers.set(listener, wrapper)
        return this.on(event, wrapper)
      }
    
      emit(event, ...args) {
        const listeners = this._events[event]
        if (!listeners?.length) return false
        listeners.slice().forEach(l => l(...args))
        return true
      }
    
      listenerCount(event) {
        return this._events[event]?.length ?? 0
      }
    }
    
    // Пример: Store на основе EventEmitter
    class Store extends EventEmitter {
      constructor(initialState) {
        super()
        this._state = initialState
      }
    
      getState() { return { ...this._state } }
    
      setState(updates) {
        const prevState = this._state
        this._state = { ...this._state, ...updates }
        this.emit('change', this._state, prevState)
      }
    }
    
    const store = new Store({ count: 0, user: null })
    
    // Подписка на изменения
    const unsubscribeLog = (newState) => {
      console.log('Изменение состояния:', newState)
    }
    store.on('change', unsubscribeLog)
    
    // Подписка once — сработает только 1 раз
    store.once('change', (state) => {
      console.log('Первое изменение (once):', state.count)
    })
    
    store.setState({ count: 1 })  // оба listener сработают
    store.setState({ count: 2 })  // только unsubscribeLog
    
    console.log('Listeners после двух изменений:', store.listenerCount('change'))  // 1
    
    // ===== SINGLETON =====
    console.log('\n=== Singleton ===')
    
    const Logger = (() => {
      let instance = null
      let logCount = 0
    
      class LoggerClass {
        constructor(prefix) {
          if (instance) return instance  // возвращаем существующий
          this.prefix = prefix
          this.logs = []
          instance = this
        }
    
        log(level, message) {
          logCount++
          const entry = {
            id: logCount,
            level,
            message,
            prefix: this.prefix,
            timestamp: new Date().toISOString()
          }
          this.logs.push(entry)
          console.log(`[${this.prefix}][${level.toUpperCase()}] ${message}`)
        }
    
        info(msg)  { this.log('info', msg) }
        warn(msg)  { this.log('warn', msg) }
        error(msg) { this.log('error', msg) }
    
        getStats() {
          return {
            total: this.logs.length,
            byLevel: this.logs.reduce((acc, l) => {
              acc[l.level] = (acc[l.level] || 0) + 1
              return acc
            }, {})
          }
        }
      }
    
      return LoggerClass
    })()
    
    const logger1 = new Logger('APP')
    const logger2 = new Logger('ДРУГОЙ')  // вернёт logger1!
    
    logger1.info('Приложение запущено')
    logger2.warn('Это пишет тот же экземпляр')
    logger1.error('Ошибка подключения')
    
    console.log('logger1 === logger2:', logger1 === logger2)  // true
    console.log('Статистика:', logger1.getStats())
    
    // ===== FACTORY =====
    console.log('\n=== Factory ===')
    
    function createNotification(type, data) {
      const base = {
        id: Math.random().toString(36).slice(2),
        createdAt: Date.now(),
        read: false,
        markRead() { this.read = true }
      }
    
      const templates = {
        success: {
          ...base,
          type: 'success',
          icon: 'check',
          title: data.title || 'Успешно',
          message: data.message,
          duration: data.duration || 3000
        },
        error: {
          ...base,
          type: 'error',
          icon: 'x',
          title: data.title || 'Ошибка',
          message: data.message,
          duration: data.duration || 0,  // ошибки не исчезают автоматически
          retry: data.retry || null
        },
        info: {
          ...base,
          type: 'info',
          icon: 'i',
          title: data.title || 'Информация',
          message: data.message,
          duration: data.duration || 5000
        }
      }
    
      if (!templates[type]) {
        throw new Error(`Неизвестный тип уведомления: ${type}`)
      }
    
      return templates[type]
    }
    
    const successNotification = createNotification('success', {
      message: 'Файл успешно загружен'
    })
    const errorNotification = createNotification('error', {
      title: 'Ошибка сети',
      message: 'Не удалось подключиться к серверу',
      retry: () => console.log('Повтор...')
    })
    
    console.log('Success:', successNotification.title, '-', successNotification.message)
    console.log('Error:', errorNotification.title, '| duration:', errorNotification.duration)
    console.log('Error id:', errorNotification.id)
    
    // ===== ПАТТЕРНЫ ВМЕСТЕ: УВЕДОМЛЕНИЯ С EVENTЕМITTER =====
    console.log('\n=== Объединяем: NotificationService ===')
    
    class NotificationService extends EventEmitter {
      constructor() {
        super()
        this.notifications = []
      }
    
      add(type, data) {
        const notification = createNotification(type, data)
        this.notifications.push(notification)
        this.emit('added', notification)
        if (notification.duration > 0) {
          setTimeout(() => this.remove(notification.id), notification.duration)
        }
        return notification
      }
    
      remove(id) {
        const index = this.notifications.findIndex(n => n.id === id)
        if (index !== -1) {
          const [removed] = this.notifications.splice(index, 1)
          this.emit('removed', removed)
        }
      }
    
      getAll() { return [...this.notifications] }
    }
    
    // Singleton — один сервис уведомлений на всё приложение
    const notificationSingleton = (() => {
      let service = null
      return {
        getInstance() {
          if (!service) service = new NotificationService()
          return service
        }
      }
    })()
    
    const ns = notificationSingleton.getInstance()
    ns.on('added',   n => console.log('+ Уведомление добавлено:', n.type, '-', n.message))
    ns.on('removed', n => console.log('- Уведомление удалено:', n.id))
    
    ns.add('success', { message: 'Сохранено' })
    ns.add('info',    { message: 'Новое сообщение от Алисы' })
    ns.add('error',   { message: 'Ошибка загрузки' })
    
    console.log('Всего уведомлений:', ns.getAll().length)