Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке патерн Strategy?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Патерн Strategy** визначає сімейство алгоритмів, загортає кожен в окремий об'єкт і дозволяє контексту перемикати їх під час виконання без зміни власного коду. ```javascript const p = new Processor(new PayPalStrategy()); p.process(100); // "Paid $100 via PayPal" p.strategy = new CardStrategy(); p.process(100); // "Paid $100 via Card" ``` **Головне:** поведінка змінюється підміною об'єкта-стратегії, а не переписуванням умов.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Патерн Strategy** - поведінковий патерн проектування, який визначає сімейство алгоритмів, загортає кожен в окремий об'єкт і дає контексту змогу перемикати їх під час виконання. ## Теорія ### TL;DR - Аналогія: зміна двигуна на піт-стопі - та сама машина (контекст), інший двигун (стратегія) для траси, траси або позадоріжжя - Головна ідея: замість if/else ланцюжків - делегування поведінки ін'єктованим об'єктам - Використовуй коли: 3+ алгоритми для однієї операції або вибір відбувається в runtime - Пропусти коли: 2 варіанти. Тернарний оператор або дві функції справляться простіше. ### Швидкий приклад ```javascript class PayPalStrategy { pay(amount) { return `Paid $${amount} via PayPal`; } } class CardStrategy { pay(amount) { return `Paid $${amount} via Card`; } } class Processor { constructor(strategy) { this.strategy = strategy; } process(amount) { return this.strategy.pay(amount); } } const p = new Processor(new PayPalStrategy()); console.log(p.process(100)); // "Paid $100 via PayPal" p.strategy = new CardStrategy(); console.log(p.process(100)); // "Paid $100 via Card" ``` `Processor` викликає `pay()` і довіряє стратегії обробити все інше. Він не перевіряє, який метод оплати ти передав. ### Головна відмінність Без Strategy метод `process()` обростає `if/else` на кожен спосіб оплати. Додаєш Crypto - відкриваєш файл, правиш блок, ризикуєш зламати вже робочу логіку. Зі Strategy пишеш один новий клас і ін'єктуєш його. Контекст не змінюється. Алгоритми розвиваються незалежно від коду, який їх викликає. ### Коли використовувати - 3+ способи зробити одну операцію (сортування, валідація, компресія, аутентифікація): Strategy - Користувач обирає поведінку в runtime (метод оплати, провайдер входу): Strategy - Поведінка змінюється залежно від конфігурації або середовища: Strategy - У тебе `switch` з 5+ варіантами, які роблять "одне і те ж по-різному": рефактор до Strategy - 2 варіанти без перемикання в runtime: тернарний оператор або два прямих виклики функцій ### Як це працює в JavaScript У V8 об'єкти стратегій є значеннями першого класу. Коли виконується `this.strategy.pay()`, рушій знаходить метод через динамічний пошук по ланцюжку прототипів у момент виклику. Ніякого зв'язування на етапі компіляції, ніякої vtable як у C++. Будь-який об'єкт з методом `pay()` підходить як стратегія. Duck typing робить те, що формальні інтерфейси роблять у Java або C#. Ключового слова `implements` не потрібно. ### Типові помилки **Відсутність валідації стратегії в конструкторі** ```javascript const p = new Processor(); // стратегію не передано p.process(100); // TypeError: Cannot read properties of undefined ``` Перевіряй одразу в конструкторі: ```javascript constructor(strategy) { if (!strategy?.pay) throw new Error('Strategy must implement pay()'); this.strategy = strategy; } ``` **Спільний стан між контекстами** ```javascript class LoggingStrategy { constructor() { this.log = []; } pay(amount) { this.log.push(amount); return 'Paid'; } } const shared = new LoggingStrategy(); const p1 = new Processor(shared); const p2 = new Processor(shared); p1.process(100); p2.process(200); // shared.log тепер [100, 200] - стан витік між обома процесорами ``` Стратегії мають бути без стану. Якщо потрібен стан, створюй новий екземпляр для кожного контексту. Це часте джерело непомітних багів у стратегіях логування та аналітики. **Надмірне ускладнення для двох варіантів** ```javascript // Непотрібно: два класи для бінарного вибору class USDStrategy { pay() { /* ... */ } } class EURStrategy { pay() { /* ... */ } } ``` Два варіанти? Пиши тернарний оператор. Патерн виправданий тільки коли варіантів 3+ і вони реально перемикаються в runtime. **Відсутність перевірки на null після динамічної заміни** ```javascript p.strategy = null; p.process(100); // крах ``` Якщо стратегії замінюються динамічно, захищайся від null: ```javascript process(amount) { return this.strategy?.pay(amount) ?? 'No strategy configured'; } ``` ### Де зустрічається - **Passport.js:** `passport.use(new LocalStrategy(...))` і `passport.use(new GoogleStrategy(...))` - кожен провайдер аутентифікації є Strategy, підключеною до одного й того ж middleware-контексту. У кожній кодовій базі з Passport.js, яку я бачив, цей патерн вже є, незалежно від того, чи команда знала його назву. - **TanStack Table v8+:** функції-компаратори, ін'єктовані через column meta, для перемикання сортування в runtime - **Node.js zlib:** `createDeflate()`, `createGzip()`, `createBrotliCompress()` - кожна функція є окремою стратегією стиснення в stream pipeline - **Lodash:** `_.sortBy` з функціями-ітераторами - функції як легковагові стратегії без накладних витрат класів ### Питання на співбесіді **Q:** Чим Strategy відрізняється від Template Method? **A:** Template Method розміщує скелет алгоритму в базовому класі і дозволяє підкласам перевизначати окремі кроки через наслідування. Strategy замінює весь алгоритм через композицію. Можна змінити поведінку без створення підкласу. **Q:** Strategy проти Factory - у чому різниця? **A:** Factory створює об'єкти. Strategy обирає, який алгоритм виконається в runtime. Вони часто з'являються разом: фабрика може створити потрібну стратегію на основі конфігурації або вхідних даних. **Q:** Коли Strategy додає зайві накладні витрати? **A:** Коли є лише 2 варіанти і немає перемикання в runtime. Для коду, де важлива продуктивність, зайва алокація об'єктів і непряма диспетчеризація методів додають вартість. У таких випадках передавай функцію напряму. `Array.sort(comparatorFn)` робить саме це без загортання в клас. **Q:** Як рефакторити if/else ланцюжок до Strategy? **A:** Витягни кожну гілку в клас з однаковою назвою методу, наприклад `execute()`. Замість умовного блоку використай контекст, який отримує стратегію через конструктор. Додавання нового варіанту означає новий клас, а не редагування існуючої логіки. **Q:** Як Strategy працює у функціональному програмуванні? **A:** Функції вищого порядку замінюють класи. `const sorter = (strategy) => (items) => [...items].sort(strategy)` - передаєш компаратор, отримуєш сортувальник. Та сама ідея, менше церемонії. **Q (senior):** У мікросервісній архітектурі, як би ти управляв стратегіями, розподіленими між сервісами? **A:** Використовуй патерн registry (реєстр) разом із service discovery. Контекст отримує потрібну стратегію через API-виклик (REST або gRPC). Додай circuit breakers для цього виклику. Якщо сервіс стратегій недоступний, повертайся до локального дефолту. Саме так системи feature flags та A/B-тестування управляють вибором стратегій у масштабі. ## Приклади ### Базовий: Взаємозамінні стратегії оплати ```javascript class PayPalStrategy { pay(amount) { return `Paid $${amount} via PayPal`; } } class CreditCardStrategy { pay(amount) { return `Paid $${amount} via Credit Card`; } } class CryptoStrategy { pay(amount) { return `Paid $${amount} via Crypto`; } } class PaymentProcessor { constructor(strategy) { if (!strategy?.pay) throw new Error('Invalid strategy'); this.strategy = strategy; } setStrategy(strategy) { this.strategy = strategy; } process(amount) { return this.strategy.pay(amount); } } const processor = new PaymentProcessor(new PayPalStrategy()); console.log(processor.process(50)); // "Paid $50 via PayPal" processor.setStrategy(new CryptoStrategy()); console.log(processor.process(50)); // "Paid $50 via Crypto" ``` Три методи оплати, жодного умовного оператора в `PaymentProcessor`. Четвертий метод - один новий клас. Більше нічого не змінюється. ### Середній рівень: Express middleware з підключуваними стратегіями аутентифікації ```javascript class EmailStrategy { async auth(req) { const { email, password } = req.body; // Симуляція запиту до БД return email === 'user@example.com' && password === 'secret'; } } class OAuthStrategy { async auth(req) { const { token } = req.body; // Симуляція валідації токена return token === 'valid-oauth-token'; } } const authMiddleware = (strategy) => async (req, res, next) => { const isValid = await strategy.auth(req); if (isValid) return next(); res.status(401).send('Unauthorized'); }; app.post('/login', authMiddleware(new EmailStrategy())); app.post('/oauth/callback', authMiddleware(new OAuthStrategy())); ``` Саме так працює Passport.js у своїй основі. Middleware викликає `auth()` і або пропускає запит далі, або відхиляє його. Додати SAML, API key або magic link - значить додати клас, а не чіпати функцію middleware. ### Просунутий рівень: React-хук із динамічним перемиканням стратегій сортування ```javascript const useSortableData = (items, initialConfig = null) => { const [sortConfig, setSortConfig] = useState(initialConfig); const comparators = { name: (a, b) => a.name.localeCompare(b.name), age: (a, b) => a.age - b.age, // Вторинне сортування: score за спаданням, потім name за алфавітом score: (a, b) => b.score - a.score || a.name.localeCompare(b.name), }; const sortedItems = useMemo(() => { if (!sortConfig?.key || !comparators[sortConfig.key]) return items; return [...items].sort(comparators[sortConfig.key]); // spread запобігає мутації }, [items, sortConfig]); return { items: sortedItems, sortConfig, setSortConfig }; }; // У компоненті: // <button onClick={() => setSortConfig({ key: 'score' })}>Сортувати за score</button> ``` Два важливі моменти. Перший: `[...items]` перед sort - `Array.sort()` мутує вихідний масив, що порушує очікування незмінності в React. Без spread кожне сортування змінює джерело даних прямо на місці. Другий: компаратор `score` використовує складне сортування - за score у зворотному порядку, потім за name за алфавітом при рівності очок. Ось де Strategy себе виправдовує: ти називаєш логіку, тестуєш окремо і підмінюєш без жодних змін у компоненті.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.