Strategy (Strategia) è un pattern di progettazione comportamentale che definisce una famiglia di algoritmi intercambiabili e colloca ciascuno di essi in una classe separata (Strategy). Il pattern consente di selezionare un algoritmo al volo: il codice client funziona attraverso un'interfaccia Strategy comune e l'implementazione concreta viene sostituita in fase di esecuzione. In iOS, il pattern viene implementato tramite Protocol + classi di strategia, in Android — tramite Interface + implementazioni. Strategy è uno dei 23 pattern GoF, ampiamente utilizzato per l'elaborazione dei pagamenti, la validazione, l'ordinamento e il filtraggio dei dati. Per maggiori dettagli — consulta la descrizione originale GoF.
Punti chiave
Strategy è uno dei 23 pattern GoF (Gang of Four), descritto nel libro "Design Patterns: Elements of Reusable Object-Oriented Software" (1994). Il pattern risolve il problema della selezione di un algoritmo in fase di esecuzione. Invece di scrivere una singola classe con molteplici istruzioni condizionali (if-else, switch), Strategy propone di estrarre ogni algoritmo in una classe separata con un'interfaccia comune. Il contesto (la classe che utilizza la strategia) contiene un riferimento all'interfaccia Strategy e delega l'esecuzione alla strategia concreta.
La struttura del pattern include tre elementi: Context contiene un riferimento a Strategy e chiama il suo metodo; Strategy (interfaccia) dichiara un metodo comune per tutti gli algoritmi; ConcreteStrategy implementa l'interfaccia e contiene l'algoritmo concreto. Il client crea la strategia desiderata e la passa al contesto tramite un costruttore, setter o parametro di metodo. Il contesto non sa quale strategia specifica viene eseguita — lavora solo con l'interfaccia.
| Componente | Ruolo | Esempio |
|---|---|---|
| Context | Contiene un riferimento a Strategy | PaymentProcessor, Sorter |
| Strategy | Interfaccia comune per gli algoritmi | Protocol PaymentStrategy |
| ConcreteStrategy | Implementazione concreta dell'algoritmo | CardPayment, PayPalPayment |
Principio Open/Closed — il principale vantaggio di Strategy. Il sistema è aperto all'estensione (può essere aggiunta una nuova strategia) e chiuso alla modifica (il codice del contesto non deve cambiare). Senza il pattern, aggiungere un nuovo algoritmo richiede la modifica della classe esistente, violando OCP e aumentando il rischio di errori di regressione. Strategy riduce anche la dimensione delle classi: invece di una classe di 200 righe con switch-case, si ottengono 6 classi di 20 righe ciascuna.
Strategy in Swift viene implementato tramite Protocol (interfaccia di strategia) e classi o struct di strategia. I protocolli Swift supportano tipi associati e vincoli generici, offrendo flessibilità nella progettazione delle strategie. Il contesto è solitamente una classe ViewModel o un servizio che accetta la strategia in init o tramite una proprietà. Il pattern è ampiamente utilizzato nei progetti iOS per la gestione degli eventi, le animazioni, la formattazione dei dati e le strategie 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 {
// Invio richiesta all'API bancaria
return PaymentResult(status: .success, transactionId: "tx_\(UUID())")
}
}
struct PayPalPaymentStrategy: PaymentStrategy {
let email: String
func pay(amount: Decimal) async throws -> PaymentResult {
// Reindirizzamento a 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)
}
}
// Utilizzo
let processor = PaymentProcessor(strategy: CardPaymentStrategy(cardNumber: "4111...", cvv: "123"))
let result = try await processor.processPayment(amount: 99.99)
Strategy in SwiftUI — il pattern si integra naturalmente con MVVM. Un ViewModel contiene una proprietà di strategia e chiama il suo metodo su azione dell'utente. La View SwiftUI riceve i dati tramite @Published o @State — la strategia nasconde i dettagli di implementazione alla View. Ad esempio, una strategia di validazione del testo (emailValidator, phoneValidator) viene scambiata in base al tipo di campo di input. La combinazione di Strategy con SwiftUI offre flessibilità senza ereditare da UIKit.
Strategy in Kotlin utilizza Interface a livello di linguaggio e interfacce funzionali (SAM) per la semplificazione. Kotlin supporta le lambda, consentendo di passare algoritmi come funzioni senza dichiarare una classe di strategia separata. In Android, il pattern viene utilizzato in ViewModel e Use Cases per isolare algoritmi di caricamento dati, caching e gestione degli errori. I progetti Android con Clean Architecture utilizzano Strategy per iniettare diverse implementazioni del repository in base ai flag (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 bancaria tramite Retrofit
return PaymentResult(success = true, transactionId = "tx_${UUID.randomUUID()}")
}
}
class PayPalPaymentStrategy(
private val email: String
) : PaymentStrategy {
override suspend fun pay(amount: BigDecimal): PaymentResult {
// Integrazione 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)
}
}
// Utilizzo in ViewModel
class CheckoutViewModel : ViewModel() {
private var processor = PaymentProcessor(CardPaymentStrategy("4111...", "123"))
fun payWithCard() {
viewModelScope.launch {
val result = processor.processPayment(BigDecimal("99.99"))
// Elaborazione del risultato
}
}
}
Strategy con Hilt/Dagger — nei progetti Android, le strategie vengono spesso iniettate tramite DI. Hilt fornisce un'implementazione concreta di PaymentStrategy tramite @Binds o @Provides. Ciò consente di cambiare strategia senza modificare il codice del contesto — basta cambiare il modulo DI per un'altra build (debug/release). Ad esempio, MockPaymentStrategy viene iniettato per il debug, una strategia bancaria reale per la produzione. La combinazione Strategy + DI offre la massima flessibilità.
Strategy vs State — strutturalmente i pattern sono identici: entrambi utilizzano la composizione con un'interfaccia e classi concrete. La differenza sta nello scopo: Strategy seleziona un algoritmo indipendente, State controlla il comportamento dell'oggetto in base al suo stato. In State, il contesto stesso cambia la strategia quando lo stato cambia; in Strategy, il contesto non controlla il cambio — il client imposta esplicitamente l'algoritmo. Le strategie non si conoscono tra loro, mentre gli stati possono transitare tra loro.
Strategy vs Command — Command incapsula una singola azione come oggetto, Strategy incapsula un insieme di algoritmi intercambiabili. Command è "cosa fare" (una singola chiamata execute), Strategy è "come farlo" (un algoritmo di più passaggi). Command viene utilizzato per code, esecuzione differita, annulla/ripeti. Strategy viene utilizzato per scegliere come eseguire un'attività in fase di esecuzione. I comandi possono essere parametrizzati con strategie, combinando entrambi i pattern.
| Caratteristica | Strategy | State | Command | Template Method |
|---|---|---|---|---|
| Scopo | Algoritmi intercambiabili | Comportamento per stato | Incapsulamento richiesta | Scheletro algoritmo |
| Cambio | Esplicitamente dal client | Automaticamente dal contesto | Dal client o coda | Per ereditarietà |
| Livello | Oggetto (composizione) | Oggetto (composizione) | Oggetto | Classe (ereditarietà) |
Strategy vs Template Method — entrambi i pattern definiscono algoritmi ma in modi diversi. Template Method utilizza l'ereditarietà: una classe base definisce lo scheletro dell'algoritmo (metodo template), le sottoclassi sovrascrivono singoli passaggi. Strategy utilizza la composizione: l'algoritmo è completamente esternalizzato in una classe separata. Template Method è più semplice per casi con struttura fissa dell'algoritmo, Strategy — quando gli algoritmi sono completamente diversi e possono cambiare dinamicamente.
Elaborazione pagamenti — l'esempio classico di Strategy. Il carrello di un negozio online contiene un elenco di articoli e il metodo di pagamento viene scelto dall'utente. Ogni metodo (carta, PayPal, Apple Pay, Google Pay, criptovaluta) è una strategia separata con una firma pay(amount) comune. Il contesto PaymentProcessor non sa esattamente come viene elaborato il pagamento — chiama il metodo comune. Aggiungere un nuovo metodo di pagamento non richiede la modifica del codice del carrello.
Validazione dati — Strategy viene utilizzato per diverse regole di validazione dello stesso campo. EmailValidatorStrategy, PhoneValidatorStrategy, AgeValidatorStrategy implementano un'interfaccia comune ValidationStrategy con il metodo validate(input). Un modulo di registrazione utilizza un insieme di strategie per verificare ogni campo. Le strategie di validazione possono essere combinate in una catena (Chain of Responsibility) o applicate tutte insieme in un ciclo. Questo sostituisce lunghi controlli if-else con una collezione di validatori polimorfici.
// Strategia di ordinamento
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) }
}
Autenticazione — nelle app mobili, le strategie di autenticazione vengono cambiate in base al provider. AuthStrategy con i metodi login(), logout(), getToken() viene implementato per EmailPasswordAuth, GoogleAuth, AppleAuth, BiometricAuth. Il contesto AuthManager accetta la strategia tramite DI o factory. Ciò consente di aggiungere nuovi provider di autenticazione senza modificare la schermata di login. Il pattern Strategy è alla base di molte librerie OAuth e Firebase Authentication.
Domande frequenti
Strategy è giustificato quando si hanno 3+ algoritmi che possono cambiare o espandersi. Se ci sono 2 algoritmi e sono stabili — un semplice if-else è meno costoso. Usa Strategy quando gli algoritmi vengono utilizzati in diverse parti dell'applicazione, quando è necessario scambiare algoritmi in fase di esecuzione o quando ogni algoritmo richiede proprie dipendenze e test.
No, sono pattern diversi con struttura simile. Strategy — il client seleziona esplicitamente un algoritmo e le strategie sono indipendenti. State — l'oggetto stesso cambia il suo comportamento quando il suo stato interno cambia e gli stati possono transitare tra loro. In State, il contesto gestisce i cambiamenti di stato; in Strategy, lo fa il codice client.
Sì, in Swift e Kotlin una strategia può essere passata come closure o lambda. Swift: typealias PaymentHandler = (Decimal) async throws -> PaymentResult. Kotlin: typealias PaymentFun = suspend (BigDecimal) -> PaymentResult. Questo semplifica il codice per casi semplici ma perde denominazione e documentazione. Per 1-2 algoritmi, una closure è sufficiente; per 4+, è meglio usare classi separate.
Ogni strategia viene testata con un test unitario separato utilizzando dipendenze mock. Il Contesto viene testato con una strategia mock — verificando che il contesto chiami il metodo della strategia e passi i parametri corretti. In Swift, usa XCTest + protocolli per i mock; in Kotlin, usa MockK o Mockito. Il vantaggio principale: ogni strategia viene testata isolatamente senza configurazione complessa.
Sì, Strategy è uno dei 23 pattern descritti nel libro "Design Patterns: Elements of Reusable Object-Oriented Software" (Gamma, Helm, Johnson, Vlissides, 1994). Appartiene al gruppo dei pattern comportamentali. Alias: Policy. Il codice d'esempio originale in Smalltalk-80 è disponibile nell'edizione originale del GoF.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche