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

Digital-студия WNDER

Что такое паттерны проектирования и зачем они нужны

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

Зачем их использовать? Во-первых, они проверены временем и сообществом. Во-вторых, они делают код более предсказуемым и поддерживаемым. В-третьих, они упрощают коммуникацию в команде. Но не стоит пихать паттерны везде — это может усложнить код. Используйте их там, где они действительно решают проблему.

Порождающие паттерны: Singleton и Factory

Порождающие паттерны отвечают за создание объектов. Они помогают сделать систему гибкой и независимой от конкретных классов.

Singleton (Одиночка)

Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. В JavaScript это можно реализовать с помощью замыкания или статического свойства.

class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
    this.data = [];
  }
  add(item) {
    this.data.push(item);
  }
  getData() {
    return this.data;
  }
}

const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true

Singleton полезен, когда нужен общий доступ к какому-то ресурсу, например, к соединению с базой данных или к конфигурации приложения. Но будьте осторожны: он вводит глобальное состояние, что может усложнить тестирование.

Factory (Фабрика)

Фабрика предоставляет интерфейс для создания объектов, но позволяет подклассам изменять тип создаваемых объектов. В JavaScript фабрику часто реализуют как функцию, возвращающую новый объект.

function createUser(type) {
  if (type === 'admin') {
    return { role: 'admin', permissions: ['read', 'write', 'delete'] };
  } else if (type === 'user') {
    return { role: 'user', permissions: ['read'] };
  } else {
    throw new Error('Unknown user type');
  }
}

const admin = createUser('admin');
const user = createUser('user');

Фабрика удобна, когда создание объекта требует сложной логики или когда нужно скрыть детали реализации.

Структурные паттерны: Adapter и Decorator

Структурные паттерны описывают, как классы и объекты объединяются в более крупные структуры.

Adapter (Адаптер)

Адаптер позволяет объектам с несовместимыми интерфейсами работать вместе. Это как переходник для розетки: вы вставляете вилку от европейского прибора в советскую розетку через адаптер.

// Старый интерфейс
class OldPrinter {
  printOld(text) {
    console.log('Old printer: ' + text);
  }
}

// Новый интерфейс
class NewPrinter {
  print(text) {
    console.log('New printer: ' + text);
  }
}

// Адаптер
class PrinterAdapter {
  constructor(oldPrinter) {
    this.oldPrinter = oldPrinter;
  }
  print(text) {
    this.oldPrinter.printOld(text);
  }
}

const oldPrinter = new OldPrinter();
const adapter = new PrinterAdapter(oldPrinter);
adapter.print('Hello'); // Old printer: Hello

Адаптер часто используется при интеграции сторонних библиотек или при работе с устаревшим кодом.

Decorator (Декоратор)

Декоратор добавляет объекту новые обязанности динамически, не изменяя его класс. В JavaScript декораторы можно реализовать с помощью функций высшего порядка или специального синтаксиса (если используется TypeScript или Babel).

function readonly(target, key, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class User {
  constructor(name) {
    this.name = name;
  }
  @readonly
  getName() {
    return this.name;
  }
}

Декораторы полезны для добавления логирования, кэширования, проверки прав доступа и т.д.

Поведенческие паттерны: Observer и Strategy

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

Observer (Наблюдатель)

Наблюдатель создаёт механизм подписки, позволяющий одним объектам следить за изменениями других. Это основа событийно-ориентированного программирования.

class Subject {
  constructor() {
    this.observers = [];
  }
  subscribe(observer) {
    this.observers.push(observer);
  }
  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log('Observer received:', data);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('Hello'); // Observer received: Hello (дважды)

Observer часто используется в UI-фреймворках, в системах обработки событий, для реактивного программирования.

Strategy (Стратегия)

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

const strategies = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b
};

function calculate(strategy, a, b) {
  return strategies[strategy](a, b);
}

console.log(calculate('add', 5, 3)); // 8
console.log(calculate('multiply', 5, 3)); // 15

Стратегия полезна, когда у вас есть несколько вариантов одного и того же действия, и вы хотите избежать множественных условий if-else.

Правило: Не используйте паттерны ради паттернов. Сначала убедитесь, что проблема действительно требует решения, и только потом применяйте подходящий паттерн. Иначе код станет сложнее, чем мог бы быть.

Сравнение паттернов: когда какой использовать

Чтобы легче было ориентироваться, вот таблица с краткими характеристиками рассмотренных паттернов.

Паттерн Тип Когда использовать
Singleton Порождающий Нужен единственный экземпляр класса (конфигурация, пул соединений)
Factory Порождающий Создание объектов со сложной логикой или скрытие конкретных классов
Adapter Структурный Интеграция несовместимых интерфейсов
Decorator Структурный Динамическое добавление обязанностей без изменения класса
Observer Поведенческий Организация подписки на события
Strategy Поведенческий Выбор алгоритма во время выполнения

Заключение

Паттерны проектирования — это мощный инструмент в руках разработчика. Они помогают писать чистый, понятный и расширяемый код. Но помните: паттерны — это не серебряная пуля. Используйте их осознанно, только когда они действительно упрощают решение задачи. Начните с малого: попробуйте применить Singleton для конфигурации или Observer для обработки событий. Со временем вы научитесь интуитивно выбирать подходящий паттерн.

Удачи в кодинге!

Студия WNDER