Strategy
Strategy — это поведенческий паттерн проектирования, который определяет семейство алгоритмов, помещает каждый из них в отдельный класс и делает их объекты взаимозаменяемыми.
Проблема
Представьте, что вы создаете приложение для навигации. В первой версии приложения вы можете строить маршруты только для автомобилей. В следующей версии вы добавляете поддержку пешеходных маршрутов. Позже вы добавляете возможность строить маршруты с использованием общественного транспорта.
Если вы посмотрите на код, то увидите гигантский условный оператор, который переключается между различными алгоритмами построения маршрутов. Хотя такой код работает, он сложен для поддержки и расширения.
Решение
Паттерн Strategy предлагает взять семейство алгоритмов и вынести их в собственные классы, называемые стратегиями. Вместо того чтобы изначальный класс сам выполнял алгоритм, он делегирует работу связанному объекту-стратегии.
Структура
Strategy
└── execute(a: any, b: any): any
ConcreteStrategyA
└── execute(a: any, b: any): any
ConcreteStrategyB
└── execute(a: any, b: any): any
Context
├── strategy: Strategy
├── setStrategy(strategy: Strategy): void
└── executeStrategy(a: any, b: any): any
Пример реализации
JavaScript
// Интерфейс стратегии
class PaymentStrategy {
pay(amount) {
throw new Error('Метод pay должен быть переопределен');
}
}
// Конкретные стратегии
class CreditCardPayment extends PaymentStrategy {
constructor(cardNumber, cvv, expiryDate) {
super();
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expiryDate = expiryDate;
}
pay(amount) {
console.log(`Оплата ${amount} руб. кредитной картой ${this.cardNumber}`);
console.log(`CVV: ${this.cvv}, срок действия: ${this.expiryDate}`);
return { success: true, method: 'credit_card', amount: amount };
}
}
class PayPalPayment extends PaymentStrategy {
constructor(email) {
super();
this.email = email;
}
pay(amount) {
console.log(`Оплата ${amount} руб. через PayPal (${this.email})`);
return { success: true, method: 'paypal', amount: amount };
}
}
class BankTransferPayment extends PaymentStrategy {
constructor(accountNumber, bankCode) {
super();
this.accountNumber = accountNumber;
this.bankCode = bankCode;
}
pay(amount) {
console.log(`Оплата ${amount} руб. банковским переводом`);
console.log(`Счет: ${this.accountNumber}, банк: ${this.bankCode}`);
return { success: true, method: 'bank_transfer', amount: amount };
}
}
class CryptocurrencyPayment extends PaymentStrategy {
constructor(walletAddress, currency) {
super();
this.walletAddress = walletAddress;
this.currency = currency;
}
pay(amount) {
console.log(`Оплата ${amount} руб. криптовалютой ${this.currency}`);
console.log(`Кошелек: ${this.walletAddress}`);
return { success: true, method: 'cryptocurrency', amount: amount };
}
}
// Контекст - платежная система
class PaymentProcessor {
constructor() {
this.strategy = null;
}
setPaymentStrategy(strategy) {
this.strategy = strategy;
}
processPayment(amount) {
if (!this.strategy) {
throw new Error('Стратегия оплаты не выбрана');
}
console.log('=== Обработка платежа ===');
const result = this.strategy.pay(amount);
console.log('Платеж обработан успешно!');
return result;
}
}
// Использование
const paymentProcessor = new PaymentProcessor();
// Разные способы оплаты
const creditCard = new CreditCardPayment('1234-5678-9012-3456', '123', '12/25');
const paypal = new PayPalPayment('user@example.com');
const bankTransfer = new BankTransferPayment('1234567890', 'SBER');
const crypto = new CryptocurrencyPayment('1A2B3C4D5E6F7G8H9I0J', 'BTC');
// Обрабатываем платежи разными способами
console.log('--- Оплата кредитной картой ---');
paymentProcessor.setPaymentStrategy(creditCard);
paymentProcessor.processPayment(1000);
console.log('\n--- Оплата через PayPal ---');
paymentProcessor.setPaymentStrategy(paypal);
paymentProcessor.processPayment(500);
console.log('\n--- Банковский перевод ---');
paymentProcessor.setPaymentStrategy(bankTransfer);
paymentProcessor.processPayment(2000);
console.log('\n--- Криптовалюта ---');
paymentProcessor.setPaymentStrategy(crypto);
paymentProcessor.processPayment(1500);
TypeScript с интерфейсами
// Интерфейс стратегии сортировки
interface SortStrategy {
sort(data: number[]): number[];
}
// Конкретные стратегии сортировки
class BubbleSortStrategy implements SortStrategy {
sort(data: number[]): number[] {
console.log('Сортировка пузырьком');
const arr = [...data];
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
for (let j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
}
class QuickSortStrategy implements SortStrategy {
sort(data: number[]): number[] {
console.log('Быстрая сортировка');
return this.quickSort([...data], 0, data.length - 1);
}
private quickSort(arr: number[], low: number, high: number): number[] {
if (low < high) {
const pivotIndex = this.partition(arr, low, high);
this.quickSort(arr, low, pivotIndex - 1);
this.quickSort(arr, pivotIndex + 1, high);
}
return arr;
}
private partition(arr: number[], low: number, high: number): number {
const pivot = arr[high];
let i = low - 1;
for (let j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
return i + 1;
}
}
class MergeSortStrategy implements SortStrategy {
sort(data: number[]): number[] {
console.log('Сортировка слиянием');
return this.mergeSort([...data]);
}
private mergeSort(arr: number[]): number[] {
if (arr.length <= 1) {
return arr;
}
const mid = Math.floor(arr.length / 2);
const left = this.mergeSort(arr.slice(0, mid));
const right = this.mergeSort(arr.slice(mid));
return this.merge(left, right);
}
private merge(left: number[], right: number[]): number[] {
const result: number[] = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i]);
i++;
} else {
result.push(right[j]);
j++;
}
}
return result.concat(left.slice(i)).concat(right.slice(j));
}
}
// Контекст - сортировщик
class Sorter {
private strategy: SortStrategy;
constructor(strategy: SortStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: SortStrategy): void {
this.strategy = strategy;
}
sort(data: number[]): number[] {
console.log(`Исходный массив: [${data.join(', ')}]`);
const startTime = performance.now();
const sorted = this.strategy.sort(data);
const endTime = performance.now();
console.log(`Отсортированный массив: [${sorted.join(', ')}]`);
console.log(`Время выполнения: ${(endTime - startTime).toFixed(2)} мс`);
return sorted;
}
}
// Использование
const data = [64, 34, 25, 12, 22, 11, 90, 5, 77, 30];
const sorter = new Sorter(new BubbleSortStrategy());
console.log('=== Тестирование различных стратегий сортировки ===');
console.log('\n--- Пузырьковая сортировка ---');
sorter.sort(data);
console.log('\n--- Быстрая сортировка ---');
sorter.setStrategy(new QuickSortStrategy());
sorter.sort(data);
console.log('\n--- Сортировка слиянием ---');
sorter.setStrategy(new MergeSortStrategy());
sorter.sort(data);
Когда использовать
Используйте Strategy, когда:
- Есть несколько способов выполнения задачи
- Нужно выбирать алгоритм во время выполнения
- Хотите избежать условных операторов для выбора алгоритма
- Нужно изолировать детали реализации алгоритмов от клиентского кода
Преимущества
- Алгоритмы можно переключать во время выполнения
- Изолирует детали реализации алгоритмов
- Принцип открытости/закрытости: можно добавлять новые стратегии
- Устраняет условные операторы для выбора алгоритма
Недостатки
- Клиенты должны знать о различных стратегиях
- Увеличивает количество классов в приложении
- Может усложнить код, если стратегии отличаются только параметрами
Отличия от других паттернов
- State — изменяет поведение объекта при изменении состояния
- Command — инкапсулирует запросы как объекты
- Template Method — определяет скелет алгоритма
Реальный пример
// Strategy для валидации форм
class ValidationStrategy {
validate(value) {
throw new Error('Метод validate должен быть переопределен');
}
}
class EmailValidation extends ValidationStrategy {
validate(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
class PhoneValidation extends ValidationStrategy {
validate(phone) {
const phoneRegex = /^\+?[\d\s\-\(\)]{10,}$/;
return phoneRegex.test(phone);
}
}
class PasswordValidation extends ValidationStrategy {
validate(password) {
return password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/\d/.test(password);
}
}
class FormValidator {
constructor() {
this.strategies = new Map();
}
addValidation(fieldName, strategy) {
this.strategies.set(fieldName, strategy);
}
validateForm(formData) {
const errors = {};
for (const [fieldName, strategy] of this.strategies) {
const value = formData[fieldName];
if (!strategy.validate(value)) {
errors[fieldName] = `Поле ${fieldName} не прошло валидацию`;
}
}
return {
isValid: Object.keys(errors).length === 0,
errors: errors
};
}
}
// Использование
const validator = new FormValidator();
validator.addValidation('email', new EmailValidation());
validator.addValidation('phone', new PhoneValidation());
validator.addValidation('password', new PasswordValidation());
const formData = {
email: 'user@example.com',
phone: '+7 (999) 123-45-67',
password: 'MyPassword123'
};
const result = validator.validateForm(formData);
console.log('Результат валидации:', result);
Strategy — это гибкость в выборе алгоритмов!