Strategy Pattern — Стратегия

Несколько взаимозаменяемых алгоритмов с одинаковым контрактом. В JS обычно реализуется как объект-словарь по ключу или функция-аргумент.

Проблема

Один и тот же тип задач решается разными способами:

  • сериализация: JSON, YAML, binary
  • сортировка: by name, by date, by price
  • рендеринг: console, markdown, HTML, PDF
  • доступ к БД: Oracle, Postgres, Mongo
  • оплата: карта, PayPal, криптовалюта

Хочется выбирать алгоритм в рантайме без if/else и switch, следуя Open-Closed Principle.

Где используется

  • Валидация форм (разные правила для разных полей)
  • Стратегии сортировки
  • Методы оплаты
  • Алгоритмы сжатия / шифрования
  • Стратегии кэширования
  • Express middleware
  • Аутентификация (OAuth, JWT, session)

Решение

  • Все стратегии имеют одинаковый интерфейс (вход/выход).
  • Клиент выбирает стратегию (по ключу или условию).
  • Использует выбранную стратегию без знания о её внутренностях.

Реализации

Через классы (классический GoF)

class PaymentStrategy {
  pay(amount) { throw new Error('abstract'); }
}

class CreditCardPayment extends PaymentStrategy {
  constructor(cardNumber) { super; this.cardNumber = cardNumber; }
  pay(amount) { return { success: true, method: 'card', last4: this.cardNumber.slice(-4) }; }
}

class PayPalPayment extends PaymentStrategy {
  constructor(email) { super; this.email = email; }
  pay(amount) { return { success: true, method: 'paypal', email: this.email }; }
}

// Контекст
class PaymentProcessor {
  constructor(strategy) { this.strategy = strategy; }
  setStrategy(strategy) { this.strategy = strategy; }
  checkout(amount) { return this.strategy.pay(amount); }
}

const proc = new PaymentProcessor(new CreditCardPayment('4111111111111111'));
proc.checkout(5000);
proc.setStrategy(new PayPalPayment('user@mail.com'));
proc.checkout(3000);

Словарь по ключу (JS-стиль)

const parsers = {
  json: JSON.parse,
  yaml: parseYaml,
  binary: v8.deserialize,
};
const data = parsers[format](input);

// Стратегия как параметр функции — FP-стиль
[5, 1, 3].sort((a, b) => a - b);              // sort принимает стратегию
[1, 2, 3].filter((x) => x > 1);               // filter принимает стратегию
[1, 2, 3].reduce((sum, x) => sum + x, 0);     // reduce — то же

Стратегии сортировки

const sortStrategies = {
  byName: (a, b) => a.name.localeCompare(b.name),
  byPrice: (a, b) => a.price - b.price,
  byPriceDesc: (a, b) => b.price - a.price,
  byDate: (a, b) => new Date(a.date) - new Date(b.date),
  byPopularity: (a, b) => b.views - a.views
};

function sortProducts(products, strategyName) {
  const s = sortStrategies[strategyName];
  if (!s) throw new Error(`Unknown strategy: ${strategyName}`);
  return [...products].sort(s);
}

Стратегии валидации

const validators = {
  required: (value) => ({ valid: value !== '' && value != null, message: 'Поле обязательно' }),
  email: (value) => ({
    valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
    message: 'Некорректный email'
  }),
  minLength: (min) => (value) => ({
    valid: value.length >= min,
    message: `Минимум ${min} символов`
  }),
  pattern: (regex, msg) => (value) => ({ valid: regex.test(value), message: msg })
};

const formRules = {
  username: [validators.required, validators.minLength(3)],
  email: [validators.required, validators.email],
  password: [validators.required, validators.minLength(8), validators.pattern(/[A-Z]/, 'Нужна заглавная')]
};

function validateField(value, rules) {
  for (const r of rules) {
    const result = r(value);
    if (!result.valid) return result;
  }
  return { valid: true };
}

Стратегии форматирования

const formatters = {
  json: (data) => JSON.stringify(data, null, 2),
  csv: (data) => {
    const headers = Object.keys(data[0]).join(',');
    const rows = data.map(r => Object.values(r).join(','));
    return [headers, ...rows].join('\n');
  },
};

function exportData(data, format) {
  const f = formatters[format];
  if (!f) throw new Error(`Format "${format}" not supported`);
  return f(data);
}

Где используется в JS-экосистеме

  • Array.prototype.sort()/filter/reduce/map — стратегия-аргумент
  • fetch + interceptors — стратегии перехвата
  • Knex.js dialects — Postgres, MySQL, SQLite клиенты
  • passport.js strategies — стратегии аутентификации (Local, OAuth, JWT)
  • i18n libraries — стратегии форматирования по локали

Подводные камни

  • Strategy vs State: Strategy — выбор алгоритма извне (клиент решает); State — смена поведения изнутри (объект решает).
  • Strategy vs Template Method: Template Method — алгоритм + шаги для переопределения через наследование; Strategy — заменяемый целиком через композицию.
  • Strategy vs Bridge: Strategy — выбор одного алгоритма; Bridge — связка двух иерархий.
  • Если стратегий 2-3 и они вряд ли вырастут — проще if-else.
  • Передача контекста стратегии: передавайте через аргументы метода, не храните ссылку на контекст в стратегии (нарушает инкапсуляцию).
  • Забыть проверку существования стратегииformatters[format] может быть undefined.

Главные тезисы автора

  • «В JS стратегия часто реализуется как коллекция, где название — ключ».
  • «Значение может быть функцией, классом или инстансом» — гибкость.
  • «Главное — контракт»: ООПшный или функциональный, вход/выход.
  • Стратегии должны быть взаимозаменяемы — иначе это не Strategy.
  • sort/filter/reduce — это применение Strategy в FP-стиле.
  • Применение: DAL поверх разных БД, рендереры одних данных.
  • Абстрактный класс через throw — типичный JS-приём.

🎓 Источники

См. также