Quando utilità e semplicità si uniscono
Uno dei pattern che gode di una notevole popolarità ed è al contempo piuttosto semplice è lo Strategy Pattern.
Membro della famiglia dei pattern comportamentali, ha il compito di gestire algoritmi, relazioni e responsabilità tra classi. Il GoF lo definisce come:
Definisce una serie di algoritmi incapsulati che possono essere scambiati per ottenere comportamenti specifici.
Vediamo il diagramma UML:
Si può notare come il Context, che può essere immaginato come qualsiasi entità abbia bisogno di un comportamento "dinamico", sia composto da una Strategy.
Perchè Strategy è un'interfaccia? Per il semplice fatto che le ConcreteStrategy implementeranno questa interfaccia in maniera tale che, nel momento in cui si decide di cambiare l'implementazione dei metodi delle classi concrete, la struttura del Context non muterà.
Parlando di classi già presenti nelle varie librerie Java, i java.awt.Container component sono un esempio di Strategy Pattern, infatti il LayoutManager agisce come classe Strategy, e le classi come BorderLayout e FlowLayout implementano LayoutManager implementando il metodo addLayoutComponent(). Le diverse implementazioni si diversificano per il modo e la posizione dell'oggetto che verrà inserito nel Container. La classe Container contiene l'oggetto LayoutManager.
Altri esempi sono:
java.util.Comparator#compare()chiamato daCollection.sort();javax.servlet.http.HttpServlet: metodoservice()con tutti i relativi metodi doXXXX() che accettano HttpServletRequest e HttpServletResponse come argomenti;javax.servlet.Filter#doFilter();
Vediamo in dettaglio le singole componenti del pattern.
- Strategy: è l'interfaccia che dichiara la famiglia di algoritmi e che viene utilizzata da Context per invocare un algoritmo concreto;
- Context: classe di contesto che invoca le ConcreteStrategy. Può esporre un'interfaccia per permettere alle ConcreteStrategy di accedere ad eventuali strutture dati interne;
- ConcreteStrategy: sono l'implementazione degli algoritmi che espone Strategy;
Cerchiamo di capire la struttura generale del pattern:
Strategy
public interface Strategy{
public void execute(paremeters);
}
ConcreteStrategy
public class ConcreteStrategyA implements Strategy{
@Override
public void execute(parameters){
//implementazione
}
}
public class ConcreteStrategyB implements Strategy{
@Override
public void execute(parameters){
//implementazione
}
}
Context
public class SortingContext {
private Strategy strategy;
public void setMethod(Strategy strategy) {
this.strategy = strategy;
}
public Strategy getStrategy() {
return strategy;
}
public void doMethod(paremters){
strategy.execute(parameters); }
}
Precisiamo il fatto che dando una struttura generale, l'interfaccia espone un solo metodo e quindi le classi che la implementano ne sovrascriveranno solo uno. Si può anche estendere la struttura esponendo più metodi astratti ed il funzionamento rimarrebbe invariato. Il codice sarebbe solo leggermente più lungo, ma non necessariamente complesso.
Vediamo uno schema di come un client mette in azione lo Strategy Pattern:
Ecco un esempio che utilizza i diversi algoritmi di ordinamento applicando lo Strategy Pattern:
Strategy
public interface SortingStrategy{
public void sort(int[] v);
}
ConcreteStrategy
public class SelectionSort implements SortingStrategy{
@Override
public void sort(int[] v){
System.out.println("Selection Sort!");
int first;
int temp;
for (int index = v.length - 1; index > 0; index--) {
first = 0;
for (int j = 1; j <= i; j++) {
if (v[j] > v[first])
first = j;
}
temp = v[first];
v[first] = v[index];
v[index] = temp;
}
System.out.println(Arrays.toString(v));
}
}
public class InsertionSort implements SortingStrategy {
@Override
public void sort(int[] v) {
System.out.println("Insertion Sort!");
for (int index = 1; index < v.length; index++) {
int temp = v[index];
int j;
for (j = index - 1; (j >= 0) && (v[j] > temp); j--) {
v[j + 1] = v[j];
}
v[j + 1] = temp;
}
System.out.println(Arrays.toString(v));
}
}
Context
public class SortingContext {
private SortingStrategy strategy;
public void setSortingMethod(SortingStrategy strategy) {
this.strategy = strategy;
}
public SortingStrategy getStrategy() {
return strategy;
}
public void sortNumbers(int[] v){
strategy.sort(v);
}
}
Capiamo il codice
- SortingStrategy: ha il comportamento classico dell'interfaccia Strategy descritta prima, infatti espone un metodo che rappresenta l'algoritmo da implementare;
- SelectionSort/InsertionSort: svolgono il ruolo del ConcreteContext. Questo perchè implementando SortingStrategy effettuano l'overriding del metodo, definendo un'implementazione dell'algoritmo di ordinamento desiderato;
- SortingContext: svolge il ruolo di Context, esponendo un oggetto SortingStrategy che successivamente diventerà un'istanza di una delle classi concrete;
Ma perchè ricorrere allo Strategy Pattern?
Capita talvolta di avere situazioni piuttosto complesse dove utilizzando meccanismi di ereditarietà, si avrebbero gerarchie piuttosto lunghe e contorte. Questo pattern permette di ridurre notevolmente la complessità del codice rendendolo più lineare. D'altro canto, tutti i client che utilizzano la nostra classe “Context” devono conoscere lo Strategy più indicato da utilizzare.
Una domanda che ora potrebbe sorgere spontanea è: "O, mi hai fatto vedere un esempio canonico con gli algoritmi di ordinamento. Praticamente, dove viene utilizzato?".
I casi d'uso sono molteplici. Possiamo pensare a software di compressione dati, i quali ci permettono di scegliere tra diversi formati di compressione. Un altro esempio è una piattaforma di pagamento dove ipoteticamente si può scegliere il metodo di pagamento da utilizzare e via discorrendo.
Questi casi, come molti altri, fanno uso dello Strategy Pattern.
Va precisato che in molti casi, organizzare bene il pattern potrebbe non essere banale.
Qualche osservazione
In definitiva, abbiamo definito una struttura generale del modello, dado la costruzione generale delle varie componenti dandone poi un esempio pratico.
Secondo me, per un qualsiasi programmatore che si voglia affacciare ad una programmazione professionale, questo è uno tra i must da sapere. Permette di linearizzare tante situazioni che sfruttando i comuni meccanismi che ci vengono insegnati a volte alle scuole superiori, come l'ereditarietà, sarebbero nettamente più complicate.
Parlando di un minimo di informatica teorica, questo pattern sfrutta pesantemente il meccanismo del polimorfismo messo a disposizione dalla presenza delle interfacce che sfruttiamo come Strategy. L'importanza di avere un'interfaccia generica, come accennato prima, è quella di poter cambiare il comportamento delle ConcreteStrategy senza andare a modificare la struttura della classe stessa, infatti sfruttiamo anche la possibilità di avere un tipo apparente diverso dal tipo reale della classe. Infatti, come sappiamo in Java è lecito dichiarare, ad esempio, un arraylist come:
List list = new ArrayList(100);
Questo principio viene utilizzato quando andiamo a dichiarare un oggetto Strategy nel context e successivamente lo istanziamo come istanza di una ConcreteStrategy, che implementa Strategy.
In definitiva, come detto prima, è qualcosa da ssapere, che può tornare veramente utile in molte situazioni.