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-приём.
🎓 Источники
- 🎓 Стратегия (Strategy) — выбор взаимозаменяемого поведения · 2019-03-19
- Несколько похожих алгоритмов с совместимыми I/O
- Рендереры таблицы как пример
- Стратегия в FP через sort/filter/reduce
- DAL поверх разных БД
- Контракт стратегии — главное
- 🎓 GoF Patterns Обзор всех паттернов · 2025-04-29
- Strategy как словарь по ключу
- Refactoring Guru — Strategy
- Patterns.dev — Strategy