Strategy (Strategia) — behawioralny wzorzec projektowania, który definiuje rodzinę wymiennych algorytmów i umieszcza każdy z nich w osobnej klasie (Strategy). Wzorzec pozwala wybrać algorytm w locie: kod kliencki działa przez wspólny interfejs Strategy, a konkretna implementacja jest podstawiana w czasie wykonania. W iOS wzorzec implementuje się przez Protocol + klasy-strategie, w Android — przez Interface + implementacje. Strategy — jeden z 23 wzorców GoF, szeroko stosowany do przetwarzania płatności, walidacji, sortowania i filtrowania danych. Więcej — w oryginalnym opisie GoF.
Najważniejsze
Strategy — jeden z 23 wzorców GoF (Gang of Four), opisany w książce «Design Patterns: Elements of Reusable Object-Oriented Software» (1994). Wzorzec rozwiązuje problem wyboru algorytmu w czasie wykonania. Zamiast pisać jedną klasę z wieloma operatorami warunkowymi (if-else, switch), Strategy proponuje wydzielenie każdego algorytmu do osobnej klasy ze wspólnym interfejsem. Kontekst (klasa używająca strategii) przechowuje referencję do interfejsu Strategy i deleguje wykonanie do konkretnej strategii.
Struktura wzorca obejmuje trzy elementy: Context (kontekst) zawiera referencję do Strategy i wywołuje jej metodę; Strategy (interfejs) deklaruje wspólną metodę dla wszystkich algorytmów; ConcreteStrategy (konkretna strategia) implementuje interfejs i zawiera konkretny algorytm. Klient tworzy potrzebną strategię i przekazuje ją do kontekstu przez konstruktor, setter lub parametr metody. Kontekst nie wie, która strategia jest wykonywana — działa tylko z interfejsem.
| Komponent | Rola | Przykład |
|---|---|---|
| Context | Zawiera referencję do Strategy | PaymentProcessor, Sorter |
| Strategy | Wspólny interfejs dla algorytmów | Protocol PaymentStrategy |
| ConcreteStrategy | Konkretna implementacja algorytmu | CardPayment, PayPalPayment |
Zasada Open/Closed — główna zaleta Strategy. System jest otwarty na rozszerzanie (można dodać nową strategię) i zamknięty na modyfikacje (nie trzeba zmieniać kodu kontekstu). Bez wzorca dodanie nowego algorytmu wymaga zmiany istniejącej klasy, co narusza OCP i zwiększa ryzyko błędów regresyjnych. Strategy zmniejsza również rozmiar klas: zamiast 200-wierszowej klasy z switch-case otrzymujemy 6 klas po 20 wierszy każda.
Strategy w Swift implementuje się przez Protocol (interfejs strategii) i klasy lub struktury-strategie. Protokoły Swift obsługują associated types i generic constraints, co daje elastyczność przy projektowaniu strategii. Kontekstem jest zazwyczaj klasa ViewModel lub serwis, który przyjmuje strategię w init lub przez właściwość. Wzorzec jest szeroko stosowany w projektach iOS do obsługi zdarzeń, animacji, formatowania danych i strategii UI.
// 1. Protokół Strategy
protocol PaymentStrategy {
func pay(amount: Decimal) async throws -> PaymentResult
}
// 2. Konkretne strategie
struct CardPaymentStrategy: PaymentStrategy {
let cardNumber: String
let cvv: String
func pay(amount: Decimal) async throws -> PaymentResult {
// Wysłanie zapytania do API banku
return PaymentResult(status: .success, transactionId: "tx_\(UUID())")
}
}
struct PayPalPaymentStrategy: PaymentStrategy {
let email: String
func pay(amount: Decimal) async throws -> PaymentResult {
// Przekierowanie do PayPal SDK
return PaymentResult(status: .success, transactionId: "pp_\(UUID())")
}
}
// 3. Kontekst
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)
}
}
// Użycie
let processor = PaymentProcessor(strategy: CardPaymentStrategy(cardNumber: "4111...", cvv: "123"))
let result = try await processor.processPayment(amount: 99.99)
Strategy w SwiftUI — wzorzec naturalnie integruje się z MVVM. ViewModel zawiera właściwość strategii i wywołuje jej metodę przy działaniu użytkownika. SwiftUI View otrzymuje dane przez @Published lub @State — strategia ukrywa szczegóły implementacji przed View. Na przykład strategia walidacji tekstu (emailValidator, phoneValidator) jest podmieniana w zależności od typu pola wejściowego. Połączenie Strategy z SwiftUI daje elastyczność bez dziedziczenia z UIKit.
Strategy w Kotlin używa Interface na poziomie języka i funkcjonalnych interfejsów (SAM) dla uproszczenia. Kotlin obsługuje lambdy, co pozwala przekazywać algorytmy jako funkcje bez deklarowania osobnej klasy strategii. W Android wzorzec stosuje się w ViewModel i Use Cases do izolacji algorytmów ładowania danych, buforowania i obsługi błędów. Projekty Android z Clean Architecture używają Strategy do wstrzykiwania różnych implementacji repozytorium w zależności od flag (mock, real, cache).
// 1. Interfejs Strategy
interface PaymentStrategy {
suspend fun pay(amount: BigDecimal): PaymentResult
}
// 2. Konkretne strategie
class CardPaymentStrategy(
private val cardNumber: String,
private val cvv: String
) : PaymentStrategy {
override suspend fun pay(amount: BigDecimal): PaymentResult {
// API bankowe przez 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. Kontekst
class PaymentProcessor(
private val strategy: PaymentStrategy
) {
fun setStrategy(strategy: PaymentStrategy): PaymentProcessor {
return PaymentProcessor(strategy)
}
suspend fun processPayment(amount: BigDecimal): PaymentResult {
return strategy.pay(amount)
}
}
// Użycie w ViewModel
class CheckoutViewModel : ViewModel() {
private var processor = PaymentProcessor(CardPaymentStrategy("4111...", "123"))
fun payWithCard() {
viewModelScope.launch {
val result = processor.processPayment(BigDecimal("99.99"))
// Przetwarzanie wyniku
}
}
}
Strategy z Hilt/Dagger — w projektach Android strategie są często wstrzykiwane przez DI. Hilt dostarcza konkretną implementację PaymentStrategy przez @Binds lub @Provides. Pozwala to zmieniać strategię bez zmiany kodu kontekstu — wystarczy zmienić moduł DI dla innej kompilacji (debug/release). Na przykład do debugowania wstrzykiwana jest MockPaymentStrategy, a do produkcji — rzeczywista strategia bankowa. Połączenie Strategy + DI daje maksymalną elastyczność.
Strategy vs State — strukturalnie wzorce są identyczne: oba używają kompozycji z interfejsem i konkretnymi klasami. Różnica polega na przeznaczeniu: Strategy wybiera niezależny algorytm, State zarządza zachowaniem obiektu w zależności od jego stanu. W State kontekst sam zmienia strategię przy zmianie stanu, w Strategy kontekst nie kontroluje przełączania — klient jawnie określa algorytm. Strategie nie wiedzą o sobie nawzajem, stany mogą przechodzić między sobą.
Strategy vs Command — Command enkapsuluje pojedyncze działanie jako obiekt, Strategy enkapsuluje zestaw wymiennych algorytmów. Command — «co zrobić» (jedno wywołanie execute), Strategy — «jak zrobić» (algorytm z kilku kroków). Command jest używane do kolejek, opóźnionego wykonania, undo/redo. Strategy — do wyboru sposobu wykonania zadania w czasie wykonania. Komendy można parametryzować strategiami, łącząc oba wzorce.
| Cecha | Strategy | State | Command | Template Method |
|---|---|---|---|---|
| Przeznaczenie | Wymienne algorytmy | Zachowanie od stanu | Enkapsulacja żądania | Szkielet algorytmu |
| Zmiana | Jawnie przez klienta | Automatycznie przez kontekst | Przez klienta lub kolejkę | Przez dziedziczenie |
| Poziom | Obiektowy (kompozycja) | Obiektowy (kompozycja) | Obiektowy | Klasowy (dziedziczenie) |
Strategy vs Template Method — oba wzorce definiują algorytmy, ale różnymi sposobami. Template Method używa dziedziczenia: klasa bazowa definiuje szkielet algorytmu (metoda szablonowa), podklasy nadpisują poszczególne kroki. Strategy używa kompozycji: algorytm jest w całości wydzielony do osobnej klasy. Template Method jest prostszy dla przypadków o ustalonej strukturze algorytmu, Strategy — gdy algorytmy są całkowicie różne i mogą się dynamicznie zmieniać.
Przetwarzanie płatności — klasyczny przykład Strategy. Koszyk sklepu internetowego zawiera listę produktów, a sposób płatności wybiera użytkownik. Każdy sposób (karta, PayPal, Apple Pay, Google Pay, kryptowaluta) — osobna strategia ze wspólną sygnaturą pay(amount). Kontekst PaymentProcessor nie wie, jak dokładnie przeprowadzana jest płatność — wywołuje wspólną metodę. Dodanie nowego sposobu płatności nie wymaga zmiany kodu koszyka.
Walidacja danych — Strategy stosuje się do różnych reguł walidacji tego samego pola. EmailValidatorStrategy, PhoneValidatorStrategy, AgeValidatorStrategy implementują wspólny interfejs ValidationStrategy z metodą validate(input). Formularz rejestracji używa zestawu strategii do sprawdzania każdego pola. Strategie walidacji można łączyć w łańcuch (Chain of Responsibility) lub stosować wszystkie naraz w pętli. Zastępuje to długie sprawdzenia if-else kolekcją polimorficznych walidatorów.
// Strategia sortowania
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) }
}
Uwierzytelnianie — w aplikacjach mobilnych strategie uwierzytelniania przełączają się w zależności od dostawcy. AuthStrategy z metodami login(), logout(), getToken() jest implementowana dla EmailPasswordAuth, GoogleAuth, AppleAuth, BiometricAuth. Kontekst AuthManager przyjmuje strategię przez DI lub fabrykę. Pozwala to dodawać nowych dostawców uwierzytelniania bez zmiany ekranu logowania. Wzorzec Strategy — podstawa wielu bibliotek OAuth i Firebase Authentication.
Często zadawane pytania
Strategy jest uzasadniony, gdy masz 3+ algorytmy, które mogą się zmieniać lub rozszerzać. Jeśli algorytmów jest 2 i są stabilne — prosty if-else jest mniej kosztowny. Używaj Strategy, gdy algorytmy są używane w różnych częściach aplikacji, gdy trzeba podmieniać algorytmy w czasie wykonania lub gdy każdy algorytm wymaga własnych zależności i testów.
Nie, to różne wzorce o podobnej strukturze. Strategy — klient jawnie wybiera algorytm, a strategie są niezależne. State — obiekt sam zmienia swoje zachowanie przy zmianie stanu wewnętrznego, a stany mogą przechodzić między sobą. W State kontekst zarządza zmianą stanu, w Strategy — kod kliencki.
Tak, w Swift i Kotlin strategię można przekazać jako domknięcie lub lambdę. Swift: typealias PaymentHandler = (Decimal) async throws -> PaymentResult. Kotlin: typealias PaymentFun = suspend (BigDecimal) -> PaymentResult. Upraszcza to kod dla prostych przypadków, ale traci się nazewnictwo i dokumentację. Dla 1-2 algorytmów — closure wystarczy, dla 4+ — lepiej osobne klasy.
Każda strategia jest testowana osobnym testem jednostkowym z mock-zależnościami. Kontekst testuje się z mock-strategią — sprawdza się, że kontekst wywołuje metodę strategii i przekazuje prawidłowe parametry. W Swift użyj XCTest + protokoły do mock, w Kotlin — MockK lub Mockito. Główna zaleta: każda strategia jest testowana w izolacji bez skomplikowanej konfiguracji.
Tak, Strategy (Strategia) — jeden z 23 wzorców opisanych w książce «Design Patterns: Elements of Reusable Object-Oriented Software» (Gamma, Helm, Johnson, Vlissides, 1994). Należy do grupy wzorców behawioralnych (Behavioral Patterns). Synonimy: Policy (Polityka). Oryginalny kod przykładu w Smalltalk-80 jest dostępny w oryginalnym wydaniu GoF.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również