14. Strategy
Design Patterns. Урок: Strategy (Стратегия)
Заголовок раздела «Design Patterns. Урок: Strategy (Стратегия)»Strategy — поведенческий паттерн, определяющий семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Позволяет менять алгоритм независимо от клиента, который его использует.
Проблема: условная логика разрастается
Заголовок раздела «Проблема: условная логика разрастается»// ❌ Логика сортировки внутри клиентского кодаclass DataSorter { sort(data: number[], algorithm: string): number[] { if (algorithm === 'bubble') { // 30 строк bubble sort } else if (algorithm === 'quick') { // 50 строк quick sort } else if (algorithm === 'merge') { // 40 строк merge sort } else if (algorithm === 'heap') { // 35 строк heap sort — добавили позже } return data; }}Каждый новый алгоритм — правка существующего класса. Трудно тестировать каждый алгоритм отдельно.
Strategy Pattern
Заголовок раздела «Strategy Pattern»// Интерфейс стратегииinterface SortStrategy { sort(data: number[]): number[]; get name(): string;}
// Конкретные стратегииclass BubbleSortStrategy implements SortStrategy { get name() { return 'Bubble Sort'; }
sort(data: number[]): number[] { const arr = [...data]; for (let i = 0; i < arr.length; i++) { for (let j = 0; j < arr.length - 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 { get name() { return 'Quick Sort'; }
sort(data: number[]): number[] { if (data.length <= 1) return data; const pivot = data[Math.floor(data.length / 2)]; const left = data.filter(x => x < pivot); const middle = data.filter(x => x === pivot); const right = data.filter(x => x > pivot); return [...this.sort(left), ...middle, ...this.sort(right)]; }}
// Контекст использует стратегиюclass DataSorter { constructor(private strategy: SortStrategy) {}
setStrategy(strategy: SortStrategy): void { this.strategy = strategy; }
sort(data: number[]): number[] { console.log(`Sorting with ${this.strategy.name}`); return this.strategy.sort(data); }}
// Переключаем стратегию в рантаймеconst sorter = new DataSorter(new BubbleSortStrategy());sorter.sort([3, 1, 4, 1, 5]); // BubbleSort
// Данных много — переключаемся на QuickSortsorter.setStrategy(new QuickSortStrategy());sorter.sort([3, 1, 4, 1, 5]); // QuickSortПрактический пример: стратегии авторизации
Заголовок раздела «Практический пример: стратегии авторизации»interface AuthStrategy { authenticate(credentials: any): Promise<User>; validate(token: string): Promise<User | null>;}
class JwtAuthStrategy implements AuthStrategy { constructor(private secret: string) {}
async authenticate(credentials: { email: string; password: string }): Promise<User> { const user = await userRepo.findByEmail(credentials.email); if (!user || !await bcrypt.compare(credentials.password, user.passwordHash)) { throw new AuthError('Invalid credentials'); } return user; }
async validate(token: string): Promise<User | null> { try { const payload = jwt.verify(token, this.secret) as { userId: string }; return await userRepo.findById(payload.userId); } catch { return null; } }}
class GoogleOAuthStrategy implements AuthStrategy { constructor(private clientId: string, private clientSecret: string) {}
async authenticate(credentials: { code: string }): Promise<User> { const tokens = await googleOAuth.exchangeCode(credentials.code); const profile = await googleOAuth.getUserProfile(tokens.accessToken); return await userRepo.findOrCreateByGoogleId(profile.id, profile); }
async validate(token: string): Promise<User | null> { try { const payload = await googleOAuth.verifyIdToken(token); return await userRepo.findByGoogleId(payload.sub); } catch { return null; } }}
class ApiKeyStrategy implements AuthStrategy { async authenticate(credentials: { apiKey: string }): Promise<User> { const keyRecord = await apiKeyRepo.findByKey(credentials.apiKey); if (!keyRecord || keyRecord.isExpired()) throw new AuthError('Invalid API key'); return await userRepo.findById(keyRecord.userId); }
async validate(token: string): Promise<User | null> { const keyRecord = await apiKeyRepo.findByKey(token); if (!keyRecord || keyRecord.isExpired()) return null; return await userRepo.findById(keyRecord.userId); }}
// Контекстclass AuthService { private strategies = new Map<string, AuthStrategy>();
register(name: string, strategy: AuthStrategy): void { this.strategies.set(name, strategy); }
async authenticate(strategyName: string, credentials: any): Promise<User> { const strategy = this.strategies.get(strategyName); if (!strategy) throw new Error(`Unknown auth strategy: ${strategyName}`); return strategy.authenticate(credentials); }}Strategy через функции (функциональный подход)
Заголовок раздела «Strategy через функции (функциональный подход)»В TypeScript стратегии часто реализуют как функции, а не классы:
// Стратегия — просто функцияtype PricingStrategy = (basePrice: number, quantity: number) => number;
const standardPricing: PricingStrategy = (price, qty) => price * qty;
const bulkDiscountPricing: PricingStrategy = (price, qty) => { const discount = qty > 100 ? 0.15 : qty > 50 ? 0.10 : qty > 20 ? 0.05 : 0; return price * qty * (1 - discount);};
const premiumPricing: PricingStrategy = (price, qty) => { return price * qty * 0.85; // 15% скидка для premium};
// Конфигурация стратегииfunction calculateOrderTotal( items: OrderItem[], pricingStrategy: PricingStrategy = standardPricing,): number { return items.reduce((total, item) => { return total + pricingStrategy(item.price, item.quantity); }, 0);}
// Легко передавать как параметрconst total = calculateOrderTotal(items, bulkDiscountPricing);Практические задания
Заголовок раздела «Практические задания»-
Создай систему валидации паролей с несколькими стратегиями:
StrictStrategy(минимум 12 символов, цифры, спецсимволы),StandardStrategy(8+ символов, буквы + цифры),LaxStrategy(любые 6+ символов). -
Реализуй
ExportStrategyс реализациямиCsvExport,JsonExport,XmlExport. -
Создай
RenderStrategyдля компонентов UI:MobileRenderStrategyиDesktopRenderStrategy. -
Чем Strategy отличается от State паттерна?
-
Как Strategy соотносится с принципом OCP? (Подсказка: они тесно связаны)