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. Конкретни стратегии
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 за инжектиране на различни имплементации на repository в зависимост от флагове (mock, real, cache).
// 1. Interface Strategy
interface PaymentStrategy {
suspend fun pay(amount: BigDecimal): PaymentResult
}
// 2. Конкретни стратегии
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 стратегията може да се предаде като closure или ламбда. Swift: typealias PaymentHandler = (Decimal) async throws -> PaymentResult. Kotlin: typealias PaymentFun = suspend (BigDecimal) -> PaymentResult. Това опростява кода за прости случаи, но се губи именуването и документацията. За 1-2 алгоритъма — closure е достатъчен, за 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). Принадлежи към групата на поведенческите модели (Behavioral Patterns). Синоними: Policy (Политика). Оригиналният примерен код на Smalltalk-80 е достъпен в оригиналното издание на GoF.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също