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-протоколы поддерживают associated types и generic constraints, что даёт гибкость при проектировании стратегий. Контекст обычно — класс 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 integration
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 алгоритмов — closure достаточно, для 4+ — лучше отдельные классы.
Каждая стратегия тестируется отдельным unit-тестом с mock-зависимостями. Context тестируется с mock-стратегией — проверяется, что контекст вызывает метод стратегии и передаёт правильные параметры. В Swift используйте XCTest + протоколы для mock, в Kotlin — MockK или Mockito. Основное преимущество: каждая стратегия тестируется изолированно без сложной настройки.
Да, Strategy (Стратегия) — один из 23 паттернов, описанных в книге «Design Patterns: Elements of Reusable Object-Oriented Software» (Gamma, Helm, Johnson, Vlissides, 1994). Относится к группе поведенческих паттернов (Behavioral Patterns). Псевдонимы: Policy (Политика). Исходный код примера на Smalltalk-80 доступен в оригинальном издании GoF.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также