Перейти к содержанию
Innovialabs

Singleton, Factory, Strategy, Observer: паттерны на практике

Практика3 мин

Коротко

Strategy подменяет алгоритм, Factory выбирает нужную реализацию, Observer рассылает события подписчикам, Singleton гарантирует один экземпляр. Каждый полезен только при конкретной проблеме, а Singleton чаще лучше заменить внедрением зависимостей.

Кратко о четырёх паттернах

ПаттернЧто решаетТипичная задача
StrategyВзаимозаменяемые алгоритмы за одним интерфейсомРазные платёжные провайдеры
FactoryСоздание нужной реализации без if/else по всему кодуВыбор провайдера по настройке
ObserverОповещение многих подписчиков об одном событииУведомления после оплаты заказа
SingletonРовно один экземпляр на всё приложениеПул подключений, конфигурация

Примеры ниже — на TypeScript, но идеи одинаковы для любого языка.

Strategy: платёжные провайдеры

Магазин принимает оплату через несколько провайдеров. Без паттерна код оформления заказа обрастает ветками под каждый. Со Strategy все провайдеры реализуют общий интерфейс:

interface PaymentProvider {
  charge(orderId: string, amount: number): Promise<string>;
}

class CardProvider implements PaymentProvider {
  async charge(orderId: string, amount: number) {
    // запрос к API провайдера
    return `card-${orderId}`;
  }
}

class WalletProvider implements PaymentProvider {
  async charge(orderId: string, amount: number) {
    return `wallet-${orderId}`;
  }
}

async function checkout(provider: PaymentProvider, orderId: string, amount: number) {
  return provider.charge(orderId, amount);
}

Когда использовать: есть 2+ варианта поведения, и их число может расти. Когда не нужно: вариант один, или различия укладываются в один параметр.

Factory: выбор реализации

Кто-то должен решить, какой провайдер создать. Фабрика собирает это решение в одном месте:

type ProviderName = "card" | "wallet";

function createProvider(name: ProviderName): PaymentProvider {
  switch (name) {
    case "card": return new CardProvider();
    case "wallet": return new WalletProvider();
  }
}

const provider = createProvider(order.paymentMethod);
await checkout(provider, order.id, order.total);

Добавляя нового провайдера, вы меняете фабрику и пишете новый класс — остальной код не трогаете.

Когда использовать: создание объекта зависит от данных или конфигурации, либо требует сложной настройки. Когда не нужно: тип всегда один — обычный new проще и понятнее.

Observer: уведомления после оплаты

После успешной оплаты нужно отправить письмо, сообщение в мессенджер и обновить CRM. Если всё это вызывать прямо из платёжного кода, он узнаёт о каждом канале. Observer разворачивает зависимость:

type Listener<T> = (payload: T) => void | Promise<void>;

class EventBus<T> {
  private listeners: Listener<T>[] = [];

  subscribe(fn: Listener<T>) {
    this.listeners.push(fn);
    return () => { this.listeners = this.listeners.filter(l => l !== fn); };
  }

  async emit(payload: T) {
    await Promise.all(this.listeners.map(l => l(payload)));
  }
}

const orderPaid = new EventBus<{ orderId: string }>();
orderPaid.subscribe(e => sendEmail(e.orderId));
orderPaid.subscribe(e => notifyMessenger(e.orderId));

Платёжный код вызывает только orderPaid.emit(...) и ничего не знает о каналах.

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

Singleton: один экземпляр

Классический вариант — класс со скрытым конструктором. В JavaScript/TypeScript проще: модуль выполняется один раз, поэтому экспорт экземпляра уже даёт единственный объект.

// db.ts
import { createPool } from "./driver";

export const db = createPool({ url: process.env.DATABASE_URL });

Когда использовать: ресурс действительно должен существовать в одном экземпляре — пул подключений, кеш, логгер. Когда не нужно: почти во всех остальных случаях. Singleton — это глобальное состояние: код неявно зависит от него, а в тестах его трудно подменить. Чаще лучше создать объект один раз при старте и передавать его через конструктор (dependency injection).

Частые ошибки

  • Вводить паттерн до появления проблемы: фабрика с одним типом, Strategy с одной стратегией.
  • Делать Singleton всё подряд «для удобства».
  • Забывать об ошибках в Observer: один упавший подписчик не должен ломать остальных незаметно.
  • Смешивать Factory и бизнес-логику — фабрика должна только создавать объекты.

Частые вопросы

Strategy описывает, как взаимозаменяемые алгоритмы подставляются через общий интерфейс. Factory отвечает за то, какой из них создать. Их часто используют вместе: фабрика выбирает стратегию.

Поможем с этим

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

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.