Strategy è un design pattern comportamentale che permette di definire una famiglia di algoritmi, metterli ciascuno in una classe separata e rendere i loro oggetti interscambiabili.
Intercambialità
Rendere degli algoritmi intercambiabili significa che è possibile sostituire un algoritmo con un altro della stessa famiglia senza modificare il contesto che utilizza l’algoritmo. Quindi la tipologia di Input e Output, e la logica interna degli algoritmi deve essere consistente.
Funzionamento
Context
La Classe che utilizza l’oggetto strategy. In particolare è responsabile della gestione degli input e output dell’oggetto strategy.
Strategy (Interface)
L’interfaccia che definisce il contratto (ovvero l’algoritmo generale) che le strategie concrete devono eseguire.
Concrete Strategies
Le classi che implementano l’Interfaccia Strategy. Ogni strategia concreta fornisce una implementazione specifica dell’algoritmo.
Client
La lasse che utilizza la classe Context. È la classe che inizia l’esecuzione dell’algoritmo chiamando la classe di contesto.
Implementazione
Nella classe contesto, identificare un algoritmo soggetto a frequenti modifiche. Può anche trattarsi di un condizionale massivo che seleziona ed esegue una variante dello stesso algoritmo in fase di esecuzione.
Dichiarare l’interfaccia della strategia comune a tutte le varianti dell’algoritmo.
Uno per uno, estrarre tutti gli algoritmi nelle loro classi. Tutte devono implementare l’interfaccia della strategia.
Nella classe context, aggiungere un campo per memorizzare un riferimento a un oggetto strategia. Fornire un setter per sostituire i valori di tale campo. Il contesto deve lavorare con l’oggetto strategia solo attraverso l’interfaccia strategia. Il contesto può definire un’interfaccia che consenta alla strategia di accedere ai suoi dati.
I clienti del contesto devono associarlo a una strategia adeguata, che corrisponda al modo in cui si aspettano che il contesto svolga il suo lavoro principale.
Esempio
Questo esempio consiste nell’implementazione dei metodi utilizzati per effettuare dei pagamenti all’interno di un app.
Partiamo da uno Stato iniziale dove non viene applicato lo Strategy Pattern,
Stato iniziale
Partiamo dallo Stato iniziale dove tutto è in unica classe, in questo caso questo metodo si occupa sia dei pagamenti via carta di credito che via PayPall.
Codice
public public class PaymentService { private int cost; private boolean includeDelivery; public void procceesPayment(String paymentMethod){ if("CreditCard".equals(paymentMethod)){ // pop-up to collect credit card info // process payment System.out.println("Paying " + getTotalCost() + " with Credit Card"); } else if("PayPal".equals(paymentMethod)){ // pop-up to collect PayPal info // process payment System.out.println("Paying " + getTotalCost() + " with PayPal"); } } public int getTotalCost(){ return cost + (includeDelivery ? 10 : 0); }}
Problemi
Codice poco leggibile.
Difficile da mantenere infatti aggiungere altri metodi di pagamento, richiede di modificare un metodo funzionante rischiando di inserire dei bug. In particolare inserire un altro metodo di pagamento in questo codice significherebbe estendere la catena di if else.
Dopo aver definito l’interfaccia, dobbiamo assicurarci che venga implementata nelle classi dei metodi di pagamento.
Codice dei metodi che implementano interfaccia (Concrete Strategy)
public class PaymentByCreditCard implements PaymentStrategy { private CreditCard card; @Override public void collectPaymentDetails() { // Pop-up to collect card details... card = new CreditCard("cardNumber", "expiryDate", "cvv"); System.out.println("Collecting Card Details..."); } @Override public boolean validatePaymentDetails() { // Validate credit card... System.out.println("Validating Card Info: " + card); return true; } @Override public void pay(int amount) { System.out.println("Paying " + amount + " using Credit Card"); card.setAmount(card.getAmount() - amount); }}
public class PaymentByPayPal implements PaymentStrategy { private String email; private String password; @Override public void collectPaymentDetails() { // Pop-up to collect PayPal mail and password... email = "PayPal Mail"; password = "PayPal Password"; System.out.println("Collecting PayPal Account Details..."); } @Override public boolean validatePaymentDetails() { // Validate account... System.out.printf("Validating PayPal Info: %s | %s%n", email, password); return true; } @Override public void pay(int amount) { System.out.println("Paying " + amount + " using PayPal"); }}
3) Implementazione del classe Context
Una volta implementata l’interfaccia Strategy, possiamo utilizzarla nel classe di contesto ovvero la classe che utilizza l’interfaccia Strategy, in questo caso PaymentService.
È importante notare che questo metodo oramai non ha più visibilità sul come la logica di pagamento avvenga, infatti questa responsabilità è stata delegata a tutti i metodi che implementano l’interfaccia strategy.
Questo è importante perché ci permette di implementare altri metodi di pagamento senza dovere toccare il resto del codice.
Codice classe Context
@Setterpublic class PaymentService { private int cost; private boolean includeDelivery = true; private PaymentStrategy strategy; public void processOrder(int cost) { this.cost = cost; strategy.collectPaymentDetails(); if (strategy.validatePaymentDetails()) { strategy.pay(getTotal()); } } private int getTotal() { return includeDelivery ? cost + 10 : cost; }}
Esempio di utilizzo (client)
Vediamo un esempio di un possibile client che utilizza le classi utilizzate per implementare PaymentService utilizzando lo Strategy Pattern.
Esempio Client
public class Main { public static void main(String[] args) { // Creo un oggetto PaymentService PaymentService paymentService = new PaymentService(); // Utilizzo la strategia di pagamento con carta di credito paymentService.setStrategy(new PaymentByCreditCard()); paymentService.processOrder(100); // Utilizzo la strategia di pagamento con PayPal paymentService.setStrategy(new PaymentByPayPal()); paymentService.processOrder(200); }}