Meta title: Pattern Strategy in Java: guida completa con esempi
Meta description: Scopri cos’è il Pattern Strategy in Java, come funziona lo Strategy Design Pattern, esempi pratici, vantaggi, svantaggi e implementazioni moderne con lambda.
Slug consigliato: pattern-strategy
Keyword principale: pattern strategy
Keyword correlate: strategy pattern java, design pattern strategy, strategy design pattern, pattern strategy java, strategy pattern in java, strategy design pattern in java, strategy pattern example java
Il Pattern Strategy è uno dei design pattern più utili nello sviluppo software orientato agli oggetti. Appartiene alla categoria dei pattern comportamentali e viene utilizzato quando una classe deve poter cambiare comportamento in modo dinamico, senza modificare continuamente il proprio codice interno.
In Java, lo Strategy Pattern permette di incapsulare una famiglia di algoritmi o comportamenti all’interno di classi separate, rendendoli intercambiabili tra loro. In questo modo, la classe principale non deve conoscere i dettagli dell’algoritmo concreto che verrà utilizzato, ma lavora con un’interfaccia comune.
Il concetto alla base del design pattern Strategy è semplice: invece di scrivere molte condizioni if, else o switch per scegliere un comportamento, si definiscono diverse strategie e si passa alla classe principale la strategia da usare.
Questo rende il codice più pulito, più flessibile, più semplice da testare e più facile da estendere nel tempo.
Cos’è il Pattern Strategy
Il Pattern Strategy è un design pattern comportamentale che consente di definire una famiglia di algoritmi, incapsularli in classi separate e renderli intercambiabili.
La definizione classica del pattern può essere riassunta così: lo Strategy Pattern permette di separare ciò che varia da ciò che rimane stabile, spostando gli algoritmi variabili fuori dalla classe che li utilizza.
In pratica, una classe principale, chiamata spesso Context, non implementa direttamente tutti i comportamenti possibili. Al contrario, delega il comportamento a un oggetto esterno che implementa una determinata interfaccia.
Questo oggetto esterno rappresenta la strategia concreta.
Il risultato è un codice più modulare. Quando bisogna aggiungere un nuovo comportamento, non è necessario modificare il Context; basta creare una nuova classe che implementa l’interfaccia della strategia.
Perché usare lo Strategy Pattern in Java
Lo Strategy Pattern Java è utile quando una classe può comportarsi in modi diversi a seconda del contesto, della configurazione, della scelta dell’utente o delle regole di business.
Un esempio classico è un sistema di pagamento. Un checkout può accettare carta di credito, PayPal, bonifico, wallet digitale o altri metodi. Il processo generale rimane lo stesso, ma cambia la logica concreta del pagamento.
Un altro esempio è l’ordinamento di una lista. Una collezione può essere ordinata per nome, prezzo, data, priorità o lunghezza del testo. L’operazione generale è sempre “ordinare”, ma l’algoritmo di confronto cambia.
Il pattern Strategy Java è quindi indicato quando:
-
esistono più varianti dello stesso comportamento;
-
una classe contiene molte condizioni per scegliere un algoritmo;
-
si vuole evitare di modificare una classe ogni volta che si aggiunge una nuova logica;
-
si vuole programmare per interfacce e non per implementazioni concrete;
-
si vuole migliorare testabilità, manutenzione ed estendibilità del codice.
Il vantaggio principale è che il codice diventa più aperto all’estensione e meno soggetto a modifiche rischiose.
Il problema senza Strategy Pattern
Per capire davvero il valore del design pattern Strategy, immaginiamo uno shop online che deve gestire diversi metodi di pagamento.
Una prima soluzione potrebbe essere quella di creare una classe PaymentProcessor con un metodo pay() che decide cosa fare in base al tipo di pagamento.
import java.math.BigDecimal;
public class PaymentProcessor {
public void pay(String paymentType, BigDecimal amount) {
if ("CREDIT_CARD".equals(paymentType)) {
System.out.println("Pagamento di " + amount + " con carta di credito");
} else if ("PAYPAL".equals(paymentType)) {
System.out.println("Pagamento di " + amount + " con PayPal");
} else if ("BANK_TRANSFER".equals(paymentType)) {
System.out.println("Pagamento di " + amount + " con bonifico bancario");
} else {
throw new IllegalArgumentException("Metodo di pagamento non supportato");
}
}
}
Questo codice funziona, ma ha diversi problemi.
Ogni volta che bisogna aggiungere un nuovo metodo di pagamento, è necessario modificare la classe PaymentProcessor. Se i metodi diventano molti, il codice si riempie di condizioni. Se la logica di ogni pagamento cresce, il metodo diventa lungo, difficile da leggere e difficile da testare.
Il problema principale è che la classe PaymentProcessor sta facendo troppe cose. Non si limita a coordinare il pagamento, ma conosce anche tutti i dettagli delle singole modalità di pagamento.
Questo viola un principio importante della progettazione software: separare le parti che variano da quelle che restano stabili.
L’idea alla base del Design Pattern Strategy
Lo Strategy Design Pattern propone una soluzione più pulita: spostare ogni algoritmo in una classe separata.
Nel nostro esempio, il pagamento con carta, il pagamento con PayPal e il pagamento con bonifico diventano strategie diverse. Tutte le strategie implementano la stessa interfaccia, ma ognuna ha una propria logica concreta.
La classe principale non deve più chiedersi “quale metodo di pagamento sto usando?”. Deve solo chiamare il metodo pay() della strategia ricevuta.
Il comportamento cambia in base all’oggetto strategia passato al contesto.
Questo approccio segue due principi fondamentali:
-
programmare per interfacce, non per implementazioni;
-
incapsulare ciò che varia.
Questi due principi rendono il codice più flessibile e meno fragile davanti a nuove richieste.
Struttura del Pattern Strategy
La struttura del Pattern Strategy è composta da tre elementi principali.
Il primo elemento è la Strategy, cioè l’interfaccia comune che definisce il comportamento. Nel nostro esempio, può essere un’interfaccia PaymentStrategy.
Il secondo elemento è la ConcreteStrategy, cioè una classe concreta che implementa una specifica variante dell’algoritmo. Per esempio, CreditCardPaymentStrategy, PayPalPaymentStrategy o BankTransferPaymentStrategy.
Il terzo elemento è il Context, cioè la classe che utilizza la strategia senza conoscerne i dettagli interni. Nel nostro caso, il Checkout o il PaymentProcessor.
La struttura generale è questa:
public interface Strategy {
void execute();
}
public class ConcreteStrategyA implements Strategy {
@Override
public void execute() {
System.out.println("Esecuzione strategia A");
}
}
public class ConcreteStrategyB implements Strategy {
@Override
public void execute() {
System.out.println("Esecuzione strategia B");
}
}
public class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void executeStrategy() {
strategy.execute();
}
}
Il Context non sa quale strategia concreta sta usando. Sa solo che l’oggetto ricevuto implementa l’interfaccia Strategy.
Strategy Pattern Java: esempio con pagamenti
Vediamo ora un esempio più realistico di Strategy Pattern in Java usando un sistema di pagamento.
Per prima cosa definiamo l’interfaccia comune.
import java.math.BigDecimal;
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
Questa interfaccia rappresenta il comportamento variabile: il pagamento.
Ora possiamo creare diverse implementazioni concrete.
import java.math.BigDecimal;
public class CreditCardPaymentStrategy implements PaymentStrategy {
@Override
public void pay(BigDecimal amount) {
System.out.println("Pagamento di " + amount + " con carta di credito");
}
}
import java.math.BigDecimal;
public class PayPalPaymentStrategy implements PaymentStrategy {
@Override
public void pay(BigDecimal amount) {
System.out.println("Pagamento di " + amount + " con PayPal");
}
}
import java.math.BigDecimal;
public class BankTransferPaymentStrategy implements PaymentStrategy {
@Override
public void pay(BigDecimal amount) {
System.out.println("Pagamento di " + amount + " con bonifico bancario");
}
}
A questo punto creiamo la classe di contesto.
import java.math.BigDecimal;
public class Checkout {
private PaymentStrategy paymentStrategy;
public Checkout(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void pay(BigDecimal amount) {
paymentStrategy.pay(amount);
}
}
Infine, usiamo il tutto nel metodo main.
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
Checkout checkout = new Checkout(new CreditCardPaymentStrategy());
checkout.pay(BigDecimal.valueOf(100));
checkout.setPaymentStrategy(new PayPalPaymentStrategy());
checkout.pay(BigDecimal.valueOf(50));
checkout.setPaymentStrategy(new BankTransferPaymentStrategy());
checkout.pay(BigDecimal.valueOf(250));
}
}
L’output sarà simile a questo:
Pagamento di 100 con carta di credito
Pagamento di 50 con PayPal
Pagamento di 250 con bonifico bancario
Questo esempio mostra il cuore del pattern Strategy Java: il comportamento può cambiare a runtime senza modificare la classe Checkout.
Perché questo approccio è migliore
Rispetto alla versione con if o switch, l’utilizzo del Pattern Strategy rende il codice più modulare.
La classe Checkout non contiene più la logica di tutti i metodi di pagamento. Si limita a delegare l’operazione alla strategia ricevuta.
Se domani bisogna aggiungere un pagamento con criptovaluta, Apple Pay, Google Pay o un buono sconto, non è necessario modificare Checkout. Basta creare una nuova classe che implementa PaymentStrategy.
Esempio:
import java.math.BigDecimal;
public class CryptoPaymentStrategy implements PaymentStrategy {
@Override
public void pay(BigDecimal amount) {
System.out.println("Pagamento di " + amount + " con criptovaluta");
}
}
La classe Checkout resta invariata.
Questo è un vantaggio importante perché riduce il rischio di rompere codice già funzionante.
Strategy Pattern e principio Open/Closed
Il design pattern Strategy è strettamente collegato al principio Open/Closed.
Secondo questo principio, una classe dovrebbe essere aperta all’estensione ma chiusa alla modifica. In altre parole, dovrebbe essere possibile aggiungere nuove funzionalità senza modificare il codice già esistente.
Lo Strategy Pattern permette proprio questo.
Nel nostro esempio, possiamo aggiungere una nuova strategia di pagamento creando una nuova classe. Non dobbiamo modificare il Checkout, non dobbiamo aggiungere nuovi else if e non dobbiamo cambiare una logica già testata.
Questo rende il sistema più stabile e più facile da evolvere.
Pattern Strategy con più comportamenti
Nell’articolo originale il problema veniva affrontato con più comportamenti: pagamento, rimborso e rimborso parziale. Questo è un caso interessante, perché mostra come il pattern Strategy possa essere usato anche per separare più algoritmi indipendenti.
Possiamo definire tre interfacce diverse:
import java.math.BigDecimal;
public interface PayStrategy {
void pay(BigDecimal amount);
}
import java.math.BigDecimal;
public interface RefundStrategy {
void refund(BigDecimal amount);
}
import java.math.BigDecimal;
public interface PartialRefundStrategy {
void partialRefund(BigDecimal amount);
}
A questo punto possiamo creare strategie concrete per ogni comportamento.
import java.math.BigDecimal;
public class CreditCardPayStrategy implements PayStrategy {
@Override
public void pay(BigDecimal amount) {
System.out.println("Pagamento di " + amount + " con carta di credito");
}
}
import java.math.BigDecimal;
public class CreditCardRefundStrategy implements RefundStrategy {
@Override
public void refund(BigDecimal amount) {
System.out.println("Rimborso di " + amount + " su carta di credito");
}
}
import java.math.BigDecimal;
public class NotAllowedRefundStrategy implements RefundStrategy {
@Override
public void refund(BigDecimal amount) {
throw new UnsupportedOperationException("Rimborso non consentito");
}
}
import java.math.BigDecimal;
public class NotAllowedPartialRefundStrategy implements PartialRefundStrategy {
@Override
public void partialRefund(BigDecimal amount) {
throw new UnsupportedOperationException("Rimborso parziale non consentito");
}
}
Ora possiamo creare un processore che combina le diverse strategie.
import java.math.BigDecimal;
public class PaymentProcessor {
private PayStrategy payStrategy;
private RefundStrategy refundStrategy;
private PartialRefundStrategy partialRefundStrategy;
public PaymentProcessor(
PayStrategy payStrategy,
RefundStrategy refundStrategy,
PartialRefundStrategy partialRefundStrategy
) {
this.payStrategy = payStrategy;
this.refundStrategy = refundStrategy;
this.partialRefundStrategy = partialRefundStrategy;
}
public void pay(BigDecimal amount) {
payStrategy.pay(amount);
}
public void refund(BigDecimal amount) {
refundStrategy.refund(amount);
}
public void partialRefund(BigDecimal amount) {
partialRefundStrategy.partialRefund(amount);
}
}
E possiamo configurare diversi comportamenti senza creare una gerarchia complicata di classi.
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
PaymentProcessor visaProcessor = new PaymentProcessor(
new CreditCardPayStrategy(),
new NotAllowedRefundStrategy(),
new NotAllowedPartialRefundStrategy()
);
visaProcessor.pay(BigDecimal.valueOf(100));
}
}
Questo approccio evita di creare decine di sottoclassi solo per combinare comportamenti diversi.
Strategy Pattern vs ereditarietà
Uno dei motivi principali per usare il Pattern Strategy è evitare un abuso dell’ereditarietà.
L’ereditarietà può essere utile quando esiste una vera relazione “è un”. Tuttavia, quando la variazione riguarda comportamenti intercambiabili, spesso la composizione è una scelta migliore.
Con l’ereditarietà, ogni nuova combinazione di comportamenti può richiedere una nuova sottoclasse. Questo porta rapidamente a una gerarchia difficile da mantenere.
Con lo Strategy Pattern, invece, i comportamenti vengono composti.
Invece di dire “questa classe eredita tutto da un’altra classe”, diciamo “questa classe usa una strategia per eseguire un certo comportamento”.
Questo approccio è più flessibile perché il comportamento può essere cambiato anche a runtime.
Strategy Pattern e composizione
Il design pattern Strategy è un esempio pratico del principio “favorire la composizione rispetto all’ereditarietà”.
La composizione consiste nel costruire oggetti complessi combinando oggetti più piccoli e specializzati.
Nel nostro esempio, PaymentProcessor non eredita da una classe astratta con tutti i possibili comportamenti. Riceve invece oggetti strategia che sanno come pagare, rimborsare o gestire operazioni specifiche.
Questo rende il codice più modulare e più vicino al principio di responsabilità singola.
Ogni strategia ha una responsabilità chiara. Il contesto coordina l’operazione, ma non contiene la logica concreta di ogni variante.
Strategy Pattern con lambda in Java
Con Java 8 e le lambda expression, lo Strategy Pattern Java può essere scritto in modo ancora più compatto quando la strategia è rappresentata da un’interfaccia funzionale.
Un’interfaccia funzionale è un’interfaccia con un solo metodo astratto.
Possiamo definire PaymentStrategy come interfaccia funzionale:
import java.math.BigDecimal;
@FunctionalInterface
public interface PaymentStrategy {
void pay(BigDecimal amount);
}
Ora possiamo passare una lambda invece di creare una classe concreta.
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
PaymentStrategy creditCard = amount ->
System.out.println("Pagamento di " + amount + " con carta di credito");
PaymentStrategy paypal = amount ->
System.out.println("Pagamento di " + amount + " con PayPal");
creditCard.pay(BigDecimal.valueOf(100));
paypal.pay(BigDecimal.valueOf(50));
}
}
Questo approccio è molto utile quando la strategia è semplice e non richiede una classe dedicata.
Naturalmente, se la logica diventa lunga o complessa, è meglio creare una classe concreta separata, più leggibile e più facile da testare.
Strategy Pattern con metodo di contesto e lambda
Possiamo anche creare una classe Checkout che riceve una strategia tramite costruttore.
import java.math.BigDecimal;
public class Checkout {
private final PaymentStrategy paymentStrategy;
public Checkout(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void pay(BigDecimal amount) {
paymentStrategy.pay(amount);
}
}
Utilizzo:
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
Checkout checkout = new Checkout(amount ->
System.out.println("Pagamento di " + amount + " con carta di credito")
);
checkout.pay(BigDecimal.valueOf(120));
}
}
In questo caso, la lambda rappresenta la strategia concreta.
Questa è una forma moderna e molto leggibile di strategy pattern in Java, soprattutto quando il comportamento è breve.
Strategy Pattern con Comparator
Un esempio reale di Pattern Strategy in Java si trova nell’uso di Comparator.
Quando ordiniamo una lista, possiamo cambiare la strategia di ordinamento passando un comparatore diverso.
Immaginiamo una classe Product.
import java.math.BigDecimal;
public class Product {
private String name;
private BigDecimal price;
public Product(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public BigDecimal getPrice() {
return price;
}
}
Ora possiamo ordinare una lista di prodotti per nome.
products.sort((p1, p2) -> p1.getName().compareTo(p2.getName()));
Oppure per prezzo.
products.sort((p1, p2) -> p1.getPrice().compareTo(p2.getPrice()));
In forma ancora più leggibile:
products.sort(Comparator.comparing(Product::getName));
products.sort(Comparator.comparing(Product::getPrice));
In questo caso, il contesto è il metodo di ordinamento, mentre il Comparator rappresenta la strategia usata per confrontare gli oggetti.
Questo è uno degli esempi più chiari di strategy pattern Java già presente nella libreria standard.
Strategy Pattern con enum
In alcuni casi, le strategie possono essere modellate con un enum. Questo approccio è utile quando il numero di strategie è limitato e noto.
import java.math.BigDecimal;
public enum DiscountStrategy {
NO_DISCOUNT {
@Override
public BigDecimal apply(BigDecimal price) {
return price;
}
},
TEN_PERCENT {
@Override
public BigDecimal apply(BigDecimal price) {
return price.multiply(BigDecimal.valueOf(0.90));
}
},
TWENTY_PERCENT {
@Override
public BigDecimal apply(BigDecimal price) {
return price.multiply(BigDecimal.valueOf(0.80));
}
};
public abstract BigDecimal apply(BigDecimal price);
}
Utilizzo:
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
BigDecimal price = BigDecimal.valueOf(100);
BigDecimal finalPrice = DiscountStrategy.TEN_PERCENT.apply(price);
System.out.println(finalPrice);
}
}
Questa soluzione può essere comoda, ma non è sempre la più flessibile. Se le strategie devono essere estese frequentemente, configurate dall’esterno o gestite tramite dependency injection, è preferibile usare interfacce e classi separate.
Strategy Pattern con Spring
In applicazioni Java moderne, il Pattern Strategy viene spesso usato insieme a Spring.
Supponiamo di avere più strategie di pagamento come bean Spring.
public interface PaymentStrategy {
String getType();
void pay(BigDecimal amount);
}
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@Component
public class CreditCardPaymentStrategy implements PaymentStrategy {
@Override
public String getType() {
return "credit-card";
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Pagamento con carta: " + amount);
}
}
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@Component
public class PayPalPaymentStrategy implements PaymentStrategy {
@Override
public String getType() {
return "paypal";
}
@Override
public void pay(BigDecimal amount) {
System.out.println("Pagamento con PayPal: " + amount);
}
}
A questo punto possiamo creare un servizio che riceve tutte le strategie disponibili.
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class PaymentService {
private final Map<String, PaymentStrategy> strategies;
public PaymentService(List<PaymentStrategy> strategies) {
this.strategies = strategies.stream()
.collect(Collectors.toMap(PaymentStrategy::getType, strategy -> strategy));
}
public void pay(String type, BigDecimal amount) {
PaymentStrategy strategy = strategies.get(type);
if (strategy == null) {
throw new IllegalArgumentException("Metodo di pagamento non supportato: " + type);
}
strategy.pay(amount);
}
}
Questo approccio è molto utile in progetti reali, perché permette di aggiungere nuove strategie semplicemente creando nuovi bean.
La classe PaymentService non deve conoscere in anticipo tutte le implementazioni concrete. Riceve l’elenco dal container e seleziona quella corretta in base al tipo richiesto.
Strategy Pattern e Dependency Injection
Il pattern Strategy si integra molto bene con la dependency injection.
Invece di creare manualmente le strategie con new, è possibile iniettarle dall’esterno. Questo rende il codice più testabile e più flessibile.
Un contesto che riceve la strategia tramite costruttore è più facile da testare, perché nei test possiamo passare una strategia finta, una mock strategy o una implementazione specifica per il caso da verificare.
Esempio:
public class PriceCalculator {
private final DiscountStrategy discountStrategy;
public PriceCalculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public BigDecimal calculate(BigDecimal price) {
return discountStrategy.apply(price);
}
}
In questo modo, PriceCalculator non dipende da una strategia concreta. Dipende solo da un’interfaccia.
Questo è uno dei motivi per cui lo Strategy Design Pattern è ancora molto attuale nelle architetture moderne.
Strategy Pattern vs State Pattern
Il Pattern Strategy viene spesso confuso con lo State Pattern. I due pattern hanno una struttura simile, ma uno scopo diverso.
Lo Strategy Pattern serve a cambiare algoritmo o comportamento in base a una scelta esterna o a una configurazione. Di solito, il contesto riceve una strategia e la usa per eseguire un’operazione.
Lo State Pattern, invece, serve a cambiare il comportamento di un oggetto in base al suo stato interno. Il cambiamento di comportamento è legato all’evoluzione dello stato dell’oggetto.
In sintesi:
-
Strategy cambia algoritmo;
-
State cambia comportamento in base allo stato;
-
Strategy è spesso scelto dal client;
-
State è spesso gestito internamente dal contesto.
Se stiamo parlando di metodi di pagamento, sconti, algoritmi di ordinamento o modalità di calcolo, probabilmente siamo davanti a uno Strategy Pattern.
Se stiamo parlando di un ordine che passa da “creato” a “pagato”, poi a “spedito” e infine a “consegnato”, potremmo essere davanti a uno State Pattern.
Strategy Pattern vs Factory Pattern
Anche il confronto tra Strategy Pattern e Factory Pattern è molto comune.
Il Factory Pattern serve a creare oggetti senza esporre direttamente la logica di creazione al client.
Lo Strategy Pattern serve invece a cambiare comportamento o algoritmo.
I due pattern possono lavorare insieme. Per esempio, una factory può scegliere quale strategia creare in base a un parametro.
public class PaymentStrategyFactory {
public PaymentStrategy getStrategy(String type) {
return switch (type) {
case "credit-card" -> new CreditCardPaymentStrategy();
case "paypal" -> new PayPalPaymentStrategy();
case "bank-transfer" -> new BankTransferPaymentStrategy();
default -> throw new IllegalArgumentException("Metodo non supportato: " + type);
};
}
}
La factory decide quale oggetto creare, mentre la strategia decide quale comportamento eseguire.
Strategy Pattern vs Template Method
Il Template Method è un altro pattern comportamentale che può sembrare simile allo Strategy.
Nel Template Method, una classe astratta definisce lo scheletro di un algoritmo e lascia alcune parti alle sottoclassi.
Nello Strategy Pattern, invece, l’algoritmo viene incapsulato in un oggetto separato e passato al contesto.
La differenza principale è che Template Method usa l’ereditarietà, mentre Strategy usa la composizione.
Se il comportamento deve essere cambiato a runtime o combinato in modo flessibile, Strategy è spesso più adatto. Se invece esiste una struttura algoritmica fissa con alcuni passaggi personalizzabili, Template Method può essere una scelta corretta.
Vantaggi del Pattern Strategy
Il Pattern Strategy offre molti vantaggi.
Il primo è la riduzione di if, else e switch complessi. La logica variabile viene spostata in classi separate, ognuna con una responsabilità chiara.
Il secondo vantaggio è l’estendibilità. Per aggiungere un nuovo comportamento basta creare una nuova strategia, senza modificare il contesto.
Il terzo vantaggio è la testabilità. Ogni strategia può essere testata in modo indipendente. Anche il contesto può essere testato passando strategie controllate.
Il quarto vantaggio è la leggibilità. Invece di avere una classe enorme con molti rami condizionali, il codice viene diviso in componenti più piccoli.
Il quinto vantaggio è la flessibilità. Il comportamento può essere scelto a runtime, configurato dall’esterno o iniettato tramite un framework.
Svantaggi del Pattern Strategy
Come ogni design pattern, anche il design pattern Strategy ha alcuni svantaggi.
Il primo è l’aumento del numero di classi. Se ogni strategia viene modellata come classe separata, il progetto può contenere molti piccoli file. Questo non è necessariamente un problema, ma deve essere gestito con una buona organizzazione dei package.
Il secondo svantaggio è che il client o il contesto deve sapere quale strategia usare, oppure deve esistere un meccanismo per selezionarla.
Il terzo è che può essere eccessivo per logiche molto semplici. Se ci sono solo due condizioni banali che non cambieranno mai, introdurre un pattern completo può essere sovraingegnerizzazione.
Il quarto è che, se usato male, può nascondere la logica in troppe classi e rendere più difficile seguire il flusso del programma.
Lo Strategy Pattern è molto utile, ma deve essere applicato quando porta davvero chiarezza e flessibilità.
Quando usare il Pattern Strategy
Conviene usare il Pattern Strategy quando esistono più modi per eseguire una stessa operazione.
È una buona scelta quando il comportamento cambia spesso, quando nuove varianti vengono aggiunte nel tempo, quando una classe contiene molte condizioni, quando si vuole ridurre l’accoppiamento e quando si vuole rendere il codice più testabile.
Esempi tipici sono:
-
metodi di pagamento;
-
algoritmi di sconto;
-
calcolo delle spedizioni;
-
ordinamento di dati;
-
compressione di file;
-
esportazione in formati diversi;
-
validazioni alternative;
-
strategie di autenticazione;
-
regole di pricing;
-
notifiche via email, SMS o push.
In tutti questi casi, la struttura generale dell’operazione rimane simile, ma cambia l’algoritmo concreto.
Quando evitare lo Strategy Pattern
Il pattern Strategy non è sempre necessario.
È meglio evitarlo quando il comportamento non cambia, quando ci sono pochissime varianti, quando le condizioni sono semplici e stabili o quando l’introduzione di molte classi renderebbe il codice meno leggibile.
È anche meglio evitarlo se il team non ha bisogno di estendere quel punto del codice e se una soluzione semplice è sufficiente.
Un buon design non consiste nell’usare pattern ovunque, ma nell’usare il pattern giusto quando risolve un problema reale.
Errori comuni nello Strategy Pattern Java
Uno degli errori più comuni è creare strategie troppo grandi. Una strategia dovrebbe avere una responsabilità chiara e limitata.
Un altro errore è inserire nel contesto dettagli che appartengono alla strategia. Se il contesto continua a contenere condizioni per distinguere tra le varie strategie, il pattern non è stato applicato correttamente.
Un altro errore è creare una gerarchia complicata di strategie quando basterebbe una lambda o una semplice interfaccia funzionale.
È anche importante evitare nomi generici. Classi come StrategyA o StrategyB sono poco leggibili. Meglio usare nomi descrittivi come CreditCardPaymentStrategy, FixedDiscountStrategy o PriceAscendingStrategy.
Infine, bisogna evitare di usare Strategy solo per moda. Il pattern deve semplificare il codice, non renderlo più complicato.
Best practice per usare il Pattern Strategy
Quando si usa il Strategy Pattern in Java, è consigliabile partire da un’interfaccia semplice e chiara.
Il nome dell’interfaccia dovrebbe rappresentare il comportamento, per esempio PaymentStrategy, DiscountStrategy, ShippingCostStrategy o NotificationStrategy.
Le strategie concrete dovrebbero essere piccole, leggibili e focalizzate su un solo algoritmo.
Il contesto dovrebbe dipendere dall’interfaccia, non dalle classi concrete.
Quando possibile, è meglio iniettare la strategia tramite costruttore, perché questo rende le dipendenze esplicite e migliora la testabilità.
Se le strategie sono semplici, si possono usare lambda e interfacce funzionali. Se invece la logica è complessa, è meglio usare classi dedicate.
Nei progetti Spring, può essere utile registrare ogni strategia come bean e selezionarla tramite una mappa, una factory o una proprietà di configurazione.
Esempio completo: strategia di sconto
Vediamo un esempio completo e semplice con una strategia di sconto.
import java.math.BigDecimal;
@FunctionalInterface
public interface DiscountStrategy {
BigDecimal apply(BigDecimal price);
}
Strategia senza sconto:
import java.math.BigDecimal;
public class NoDiscountStrategy implements DiscountStrategy {
@Override
public BigDecimal apply(BigDecimal price) {
return price;
}
}
Strategia con sconto percentuale:
import java.math.BigDecimal;
public class PercentageDiscountStrategy implements DiscountStrategy {
private final BigDecimal percentage;
public PercentageDiscountStrategy(BigDecimal percentage) {
this.percentage = percentage;
}
@Override
public BigDecimal apply(BigDecimal price) {
BigDecimal discount = price.multiply(percentage);
return price.subtract(discount);
}
}
Classe di contesto:
import java.math.BigDecimal;
public class PriceCalculator {
private final DiscountStrategy discountStrategy;
public PriceCalculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public BigDecimal calculate(BigDecimal price) {
return discountStrategy.apply(price);
}
}
Utilizzo:
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
PriceCalculator calculator = new PriceCalculator(
new PercentageDiscountStrategy(BigDecimal.valueOf(0.10))
);
BigDecimal finalPrice = calculator.calculate(BigDecimal.valueOf(100));
System.out.println(finalPrice);
}
}
Output:
90.00
Lo stesso esempio può essere scritto anche con una lambda:
PriceCalculator calculator = new PriceCalculator(
price -> price.subtract(price.multiply(BigDecimal.valueOf(0.10)))
);
Questa versione è più compatta, ma conviene usarla solo quando la logica è davvero semplice.
Domande frequenti sul Pattern Strategy
Che cos’è il Pattern Strategy?
Il Pattern Strategy è un design pattern comportamentale che permette di definire una famiglia di algoritmi, incapsularli in classi separate e renderli intercambiabili.
A cosa serve lo Strategy Pattern in Java?
Lo Strategy Pattern in Java serve a cambiare comportamento o algoritmo senza modificare la classe che lo utilizza. È utile quando esistono più varianti della stessa operazione.
Qual è un esempio di Strategy Pattern Java?
Un esempio tipico è un sistema di pagamento in cui il checkout può usare diverse strategie: carta di credito, PayPal, bonifico o wallet digitale.
Qual è la differenza tra Strategy Pattern e if else?
Con if else, la classe contiene direttamente tutte le condizioni e tutte le logiche. Con lo Strategy Pattern, ogni comportamento viene spostato in una classe separata che implementa una stessa interfaccia.
Strategy Pattern e State Pattern sono uguali?
No. Lo Strategy Pattern cambia algoritmo in base a una scelta o configurazione. Lo State Pattern cambia comportamento in base allo stato interno dell’oggetto.
Il Pattern Strategy usa ereditarietà o composizione?
Il Pattern Strategy usa principalmente composizione. Il contesto riceve una strategia e le delega il comportamento, invece di ereditare direttamente da una classe base.
Si può usare lo Strategy Pattern con lambda?
Sì. In Java, se la strategia è rappresentata da un’interfaccia funzionale, può essere implementata con una lambda expression.
Comparator è un esempio di Strategy Pattern?
Sì. Comparator può essere visto come un esempio di Strategy Pattern, perché permette di cambiare la logica di confronto usata per ordinare oggetti.
Quando conviene usare il Pattern Strategy?
Conviene usarlo quando ci sono più algoritmi intercambiabili, quando una classe contiene troppe condizioni o quando si vuole rendere il codice più estendibile e testabile.
Quando non conviene usare lo Strategy Pattern?
Non conviene usarlo quando la logica è molto semplice, quando le varianti sono poche e stabili o quando introdurre molte classi renderebbe il codice più complesso senza un reale beneficio.
Conclusione
Il Pattern Strategy è uno dei design pattern più importanti e pratici nello sviluppo Java. Permette di separare gli algoritmi dalla classe che li utilizza, rendendo il codice più flessibile, leggibile e facile da mantenere.
Invece di concentrare tutte le varianti di comportamento in una sola classe piena di condizioni, lo Strategy Design Pattern sposta ogni comportamento in una strategia separata. Il contesto lavora con un’interfaccia comune e può cambiare comportamento semplicemente ricevendo una strategia diversa.
Nel mondo Java moderno, il pattern Strategy Java è ancora più potente grazie a interfacce funzionali, lambda expression, Comparator, dependency injection e framework come Spring.
Usato correttamente, il design pattern Strategy aiuta a scrivere codice più pulito, estendibile e testabile. Non deve essere applicato in modo automatico a ogni problema, ma quando esistono più algoritmi intercambiabili rappresenta una delle soluzioni più eleganti e robuste.