Strategy (Стратегія) — поведінковий патерн проєктування, який визначає сімейство взаємозамінних алгоритмів і поміщає кожен із них в окремий клас (Strategy). Патерн дозволяє вибирати алгоритм на льоту: клієнтський код працює через загальний інтерфейс Strategy, а конкретна реалізація підставляється в рантаймі. В iOS патерн реалізується через Protocol + класи-стратегії, в Android — через Interface + імплементації. Strategy — один із 23 патернів GoF, широко застосовується для обробки платежів, валідації, сортування та фільтрації даних. Докладніше — в оригінальному описі GoF.
Головне
Strategy — один із 23 патернів GoF (Gang of Four), описаний у книзі «Design Patterns: Elements of Reusable Object-Oriented Software» (1994). Патерн вирішує задачу вибору алгоритму в рантаймі. Замість того щоб писати один клас із безліччю умовних операторів (if-else, switch), Strategy пропонує виділити кожен алгоритм в окремий клас із загальним інтерфейсом. Контекст (клас, що використовує стратегію) зберігає посилання на інтерфейс Strategy і делегує виконання конкретній стратегії.
Структура патерну включає три елементи: Context (контекст) містить посилання на Strategy та викликає її метод; Strategy (інтерфейс) оголошує загальний метод для всіх алгоритмів; ConcreteStrategy (конкретна стратегія) реалізує інтерфейс та містить конкретний алгоритм. Клієнт створює потрібну стратегію та передає її в контекст через конструктор, сеттер або параметр методу. Контекст не знає, яка саме стратегія виконується — він працює лише з інтерфейсом.
| Компонент | Роль | Приклад |
|---|---|---|
| Context | Містить посилання на Strategy | PaymentProcessor, Sorter |
| Strategy | Загальний інтерфейс для алгоритмів | Protocol PaymentStrategy |
| ConcreteStrategy | Конкретна реалізація алгоритму | CardPayment, PayPalPayment |
Принцип Open/Closed — основна перевага Strategy. Система відкрита для розширення (можна додати нову стратегію) та закрита для зміни (не потрібно змінювати код контексту). Без патерну додавання нового алгоритму вимагає зміни існуючого класу, що порушує OCP та збільшує ризик регресійних помилок. Strategy також зменшує розмір класів: замість 200-рядкового класу з switch-case виходить 6 класів по 20 рядків кожен.
Strategy на Swift реалізується через Protocol (інтерфейс стратегії) та класи або структури-стратегії. Swift-протоколи підтримують асоційовані типи та обмеження узагальнень, що дає гнучкість при проєктуванні стратегій. Контекст зазвичай — клас ViewModel або сервіс, який приймає стратегію в init або через властивість. Патерн широко використовується в iOS-проєктах для обробки подій, анімацій, форматування даних та UI-стратегій.
// 1. Protocol Strategy
protocol PaymentStrategy {
func pay(amount: Decimal) async throws -> PaymentResult
}
// 2. Concrete Strategies
struct CardPaymentStrategy: PaymentStrategy {
let cardNumber: String
let cvv: String
func pay(amount: Decimal) async throws -> PaymentResult {
// Відправка запиту в банківський API
return PaymentResult(status: .success, transactionId: "tx_\(UUID())")
}
}
struct PayPalPaymentStrategy: PaymentStrategy {
let email: String
func pay(amount: Decimal) async throws -> PaymentResult {
// Перенаправлення на PayPal SDK
return PaymentResult(status: .success, transactionId: "pp_\(UUID())")
}
}
// 3. Context
class PaymentProcessor {
private var strategy: PaymentStrategy
init(strategy: PaymentStrategy) {
self.strategy = strategy
}
func setStrategy(_: PaymentStrategy) {
strategy = strategy
}
func processPayment(amount: Decimal) async throws -> PaymentResult {
return try await strategy.pay(amount: amount)
}
}
// Використання
let processor = PaymentProcessor(strategy: CardPaymentStrategy(cardNumber: "4111...", cvv: "123"))
let result = try await processor.processPayment(amount: 99.99)
Strategy в SwiftUI — патерн природно інтегрується з MVVM. ViewModel містить властивість стратегії та викликає її метод при дії користувача. SwiftUI View отримує дані через @Published або @State — стратегія приховує деталі реалізації від View. Наприклад, стратегія валідації тексту (emailValidator, phoneValidator) підміняється залежно від типу поля введення. Комбінування Strategy з SwiftUI дає гнучкість без наслідування від UIKit.
Strategy на Kotlin використовує Interface на рівні мови та функціональні інтерфейси (SAM) для спрощення. Kotlin підтримує лямбди, що дозволяє передавати алгоритми як функції без оголошення окремого класу стратегії. В Android патерн застосовується в ViewModel та Use Cases для ізоляції алгоритмів завантаження даних, кешування та обробки помилок. Android-проєкти з Clean Architecture використовують Strategy для інжекції різних реалізацій репозиторію залежно від прапорців (mock, real, cache).
// 1. Interface Strategy
interface PaymentStrategy {
suspend fun pay(amount: BigDecimal): PaymentResult
}
// 2. Concrete Strategies
class CardPaymentStrategy(
private val cardNumber: String,
private val cvv: String
) : PaymentStrategy {
override suspend fun pay(amount: BigDecimal): PaymentResult {
// Банківський API через Retrofit
return PaymentResult(success = true, transactionId = "tx_${UUID.randomUUID()}")
}
}
class PayPalPaymentStrategy(
private val email: String
) : PaymentStrategy {
override suspend fun pay(amount: BigDecimal): PaymentResult {
// Інтеграція PayPal SDK
return PaymentResult(success = true, transactionId = "pp_${UUID.randomUUID()}")
}
}
// 3. Context
class PaymentProcessor(
private val strategy: PaymentStrategy
) {
fun setStrategy(strategy: PaymentStrategy): PaymentProcessor {
return PaymentProcessor(strategy)
}
suspend fun processPayment(amount: BigDecimal): PaymentResult {
return strategy.pay(amount)
}
}
// Використання в ViewModel
class CheckoutViewModel : ViewModel() {
private var processor = PaymentProcessor(CardPaymentStrategy("4111...", "123"))
fun payWithCard() {
viewModelScope.launch {
val result = processor.processPayment(BigDecimal("99.99"))
// Обробка результату
}
}
}
Strategy з Hilt/Dagger — в Android-проєктах стратегії часто інжектяться через DI. Hilt надає конкретну реалізацію PaymentStrategy через @Binds або @Provides. Це дозволяє змінювати стратегію без зміни коду контексту — достатньо змінити модуль DI для іншої збірки (debug/release). Наприклад, для налагодження інжектиться MockPaymentStrategy, для продакшну — реальна банківська стратегія. Комбінація Strategy + DI дає максимальну гнучкість.
Strategy vs State — структурно патерни ідентичні: обидва використовують композицію з інтерфейсом та конкретними класами. Різниця в призначенні: Strategy вибирає незалежний алгоритм, State керує поведінкою об'єкта залежно від його стану. В State контекст сам змінює стратегію при зміні стану, в Strategy контекст не контролює перемикання — клієнт явно задає алгоритм. Стратегії не знають одна про одну, стани — можуть переходити між собою.
Strategy vs Command — Command інкапсулює одну дію як об'єкт, Strategy інкапсулює набір взаємозамінних алгоритмів. Command — «що зробити» (один виклик execute), Strategy — «як зробити» (алгоритм із кількох кроків). Command використовується для черг, відкладеного виконання, undo/redo. Strategy — для вибору способу виконання завдання в рантаймі. Команди можна параметризувати стратегіями, комбінуючи обидва патерни.
| Характеристика | Strategy | State | Command | Template Method |
|---|---|---|---|---|
| Призначення | Взаємозамінні алгоритми | Поведінка від стану | Інкапсуляція запиту | Скелет алгоритму |
| Зміна | Клієнтом явно | Контекстом автоматично | Клієнтом або чергою | Наслідуванням |
| Рівень | Об'єктний (композиція) | Об'єктний (композиція) | Об'єктний | Класовий (наслідування) |
Strategy vs Template Method — обидва патерни визначають алгоритми, але різними способами. Template Method використовує наслідування: базовий клас визначає скелет алгоритму (шаблонний метод), підкласи перевизначають окремі кроки. Strategy використовує композицію: алгоритм повністю винесений в окремий клас. Template Method простіший для випадків із фіксованою структурою алгоритму, Strategy — коли алгоритми повністю різні та можуть змінюватися динамічно.
Обробка платежів — класичний приклад Strategy. Кошик інтернет-магазину містить список товарів, а спосіб оплати вибирається користувачем. Кожен спосіб (карта, PayPal, Apple Pay, Google Pay, криптовалюта) — окрема стратегія із загальною сигнатурою pay(amount). Контекст PaymentProcessor не знає, як саме проводиться платіж — він викликає загальний метод. Додавання нового способу оплати не вимагає зміни коду кошика.
Валідація даних — Strategy застосовується для різних правил валідації одного поля. EmailValidatorStrategy, PhoneValidatorStrategy, AgeValidatorStrategy реалізують загальний інтерфейс ValidationStrategy з методом validate(input). Форма реєстрації використовує набір стратегій для перевірки кожного поля. Стратегії валідації можна комбінувати ланцюжком (Chain of Responsibility) або застосовувати всі відразу в циклі. Це замінює довгі if-else перевірки на колекцію поліморфних валідаторів.
// Стратегія сортування
protocol SortingStrategy {
func sort<T>(_ items: [T]) -> [T] where T: Comparable
}
struct QuickSortStrategy: SortingStrategy {
func sort<T>(_ items: [T]) -> [T] { /* quicksort */ items }
}
struct MergeSortStrategy: SortingStrategy {
func sort<T>(_ items: [T]) -> [T] { /* mergesort */ items }
}
class SortedDataSource<T> {
private var strategy: SortingStrategy
func display(_ items: [T]) { let sorted = strategy.sort(items) }
}
Аутентифікація — в мобільних застосунках стратегії аутентифікації перемикаються залежно від провайдера. AuthStrategy з методами login(), logout(), getToken() реалізується для EmailPasswordAuth, GoogleAuth, AppleAuth, BiometricAuth. Контекст AuthManager приймає стратегію через DI або фабрику. Це дозволяє додавати нових провайдерів аутентифікації без зміни екрана логіну. Патерн Strategy — основа для багатьох OAuth-бібліотек та Firebase Authentication.
Часті запитання
Strategy виправданий, коли у вас 3+ алгоритми, які можуть змінюватися або розширюватися. Якщо алгоритмів 2 і вони стабільні — простий if-else менш затратний. Використовуйте Strategy, коли алгоритми використовуються в різних частинах застосунку, коли потрібно підміняти алгоритми в рантаймі або коли кожен алгоритм потребує власних залежностей і тестів.
Ні, це різні патерни зі схожою структурою. Strategy — клієнт явно вибирає алгоритм, і стратегії незалежні. State — об'єкт сам змінює свою поведінку при зміні внутрішнього стану, і стани можуть переходити один в одного. В State контекст керує зміною стану, в Strategy — клієнтський код.
Так, в Swift та Kotlin стратегію можна передати як замикання або лямбду. Swift: typealias PaymentHandler = (Decimal) async throws -> PaymentResult. Kotlin: typealias PaymentFun = suspend (BigDecimal) -> PaymentResult. Це спрощує код для простих випадків, але втрачається іменування та документація. Для 1-2 алгоритмів — замикання достатньо, для 4+ — краще окремі класи.
Кожна стратегія тестується окремим unit-тестом із mock-залежностями. Контекст тестується з mock-стратегією — перевіряється, що контекст викликає метод стратегії та передає правильні параметри. В Swift використовуйте XCTest + протоколи для mock, в Kotlin — MockK або Mockito. Основна перевага: кожна стратегія тестується ізольовано без складного налаштування.
Так, Strategy (Стратегія) — один із 23 патернів, описаних у книзі «Design Patterns: Elements of Reusable Object-Oriented Software» (Gamma, Helm, Johnson, Vlissides, 1994). Належить до групи поведінкових патернів. Псевдоніми: Policy (Політика). Вихідний код прикладу на Smalltalk-80 доступний в оригінальному виданні GoF.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.