Singleton, Factory, Strategy, Observer: паттерны на практике
Практика3 минРедакция Innovialabs
Коротко
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 и бизнес-логику — фабрика должна только создавать объекты.