Si tratta probabilmente di uno dei pattern (comportamentali) più famosi tra quelli teorizzati dalla banda dei quattro sul loro libro.
Il modello ha come scopo, una volta individuata una famiglia di algoritmi, di incapsularli rendendoli intercambiabili.
Il termine famiglia introdotto dalla GoF, potrebbe risultare a prima vista poco chiaro, tuttavia, lo dobbiamo intendere con un set di comportamenti.
In un qualsiasi progetto che si basa su un certo insieme di comportamenti può essere usato il design pattern Strategy incapsulando i comportamenti nelle strategie.
I comportamenti richiedono un qualche tipo di algoritmo per renderli funzionali. Incapsulandoli in strategie concrete, essi possono essere utilizzati, riutilizzati, e modificati.
Il design pattern Strategy, dunque, mette questi comportamenti (algoritmi) in classi concrete separate, che implementano un’interfaccia comune.
Partecipanti
Niente di complicato, tuttavia se ad una prima lettura potrebbero sembrare poco chiari, non allarmarti, questo elenco ti servirà nel corso dell’articolo per capire veramente il modello.
- Strategy: Interfaccia implementata da tutti gli algoritmi.
Contex
la utilizzerà per invocare un algoritmo definito con unConcreteStrategy
. - ConcreteStrategy: Classe concreta, che implementerà un algoritmo usando l’interfaccia
Strategy
. - Contex: Contiene un oggetto
ConcreteStrategy
e utilizza un riferimento aStrategy
per eseguire l’algoritmo concreto.
Struttura
Come possiamo notare dal diagramma, Context
ha una relazione di aggregazione con l’interfaccia Strategy
, dalla quale ereditano (in caso di classe astratta) o implementano (in caso di interfaccia in senso stretto) le varie ConcreteStrategy
.
Tanto per capirci, Contex
conterrà un riferimento ad una Strategy
e la utilizzerà per eseguire l’algoritmo.
Context
Nello strategy pattern, Context
ha la funzione di separare una richiesta da una strategia concreta, consentendo così alla strategia ed alla richiesta di agire indipendentemente l’una dall’altra.
Context
non è un’interfaccia (intesa come classe astratta o interfaccia vera e propria), ma si aggrega con l’interfaccia di strategia.
La Banda dei Quattro specifica le seguenti caratteristiche:
- E’ configurato con un oggetto
ConcreteStrategy
.- Contiene un riferimento ad un oggetto
Strategy
.- Può definire un’interfaccia che permetta a
Strategy
di accedere ai suoi dati.
Ecco un esempio di Context
;
<?php class Context { private $_strategy; public function __construct(IStrategy $strategy) { $this->_strategy = $strategy; } public function algorithm() { $this->_strategy->algorithm(); } }
Dal codice precedente notiamo:
- Che il costruttore prevede come parametro un oggetto
IStrategy
(Strategy
). - Che salva e mantine un riferimento a tale oggetto nel suo attributo privato
$_staregy
. - Che il metodo
algorithm()
implementa il metodo diIStrategy
(anch’esso chiamatoalgorithm()
).
Strategy
L’interfaccia IStrategy
prevede l’implementazione di un unico metodo algorithm()
.
<?php interface IStrategy { public function algorithm(); }
Ogni strategia concreta dovrà implementare questo metodo.
Strategie concrete
Come detto ogni strategia concreta (ConcreteStretegy
) dovrà implementare l’interfaccia IStrategy
(Strategy
) fornendo al metodo algorithm()
la propria implementazione:
<?php class ConcreteStretegyA implements IStrategy { public function algorithm() { //Implementazione algoritmo... } } class ConcreteStretegyB implements IStrategy { public function algorithm() { //Implementazione algoritmo... } } //...
Client
Le seguenti due linee di codice sono sufficientemente autoesplicative su come il client utilizzerà il pattern:
$context = new Context(new ConcreteStrategyA()); $context->algorithm();
Il comportamento appena visto ci rivela esattamente come funziona il poliformismo.
Ogni richiesta per un determinato algoritmo (comportamento) viene eseguita attraverso un’istanza di Context
, così le richieste di tutti gli algoritmi sembrano esattamente uguali: $context->algorithm()
.
Istruzioni condizionali
Una delle tante caratteristiche del pattern Strategy è che il partecipante Context
eviterà l’utilizzo di istruzioni condizionali per la selezione di un comportamento desiderato.
Questo non significa che le istruzioni condizionali non possono far parte del processo di selezione del client, ma significa invece che non sono parte di Context
.
Da tenere presente che poiché sarà il client a richiede le strategie concrete attraverso il contesto, questo (il client) dovrà essere consapevole delle strategie disponibili.
[box type=”note”]Il pattern Strategy impiega i principi del poliformismo al fine di prevenire l’utilizzo di istruzioni condizionali ricorrenti nel codice.[/box]
Strategie di implementazione
Le interfacce di Strategy
e Contex
dovranno fornire un modo affinchè (se necessario) ConcreteStrategy
possa attingere a tutte le info necessarie di Context
(e viceversa eventualmente).
Possiamo ottenere questo risultato facendo in modo che Context
passi i dati necessari a Strategy
rendendoli così disaccoppiati, oppure più semplicemente facendo in modo che Context
passi direttamente se stesso a Strategy
, il quale sarà così in grado di attenere tutti i dati che vuole. Qualcosa tipo:
//Context class Context { private $_strategy; public function __construct(IStrategy $strategy) { $this->_strategy = $strategy; } public function algorithm() { $this->_strategy->algorithm($this); } } //Strategy interface IStrategy { public function algorithm($context); }
[box type=”note”]Nota: l’interfaccia Strategy dovrà essere definita in modo tale da poter supportare qualsiasi algoritmo.[/box]
Uno dei svantaggi di questo pattern è che il client sarà obbligato a conoscere le strategie (visto che dovrà instanziare una ConcreteStrategy
per configurare Context
). Possiamo rendere opzionale questo assegnando a Context
un comportamento di default. In questo caso se Context
non contiene un oggetto Strategy
utilizzerà l’algoritmo di default. Il client gestiranno oggetti Strategy
solo se vogliono cambiare il comportamento di default.
class Context { private $_strategy; public function __construct(IStrategy $strategy) { $this->_strategy = $strategy; } public function algorithm() { if (!isset($this->_strategy)) { return 'My algorithm'; } $this->_strategy->algorithm($this); } }
Ereditarietà o composizione?
La possibilità di implementare svariati comportamenti (algoritmi) ci viene offerta anche dall’ereditarietà. Potremmo estendere direttamente una classe Context
per ottenere dei comportamenti diversi.
In un mio precedente articolo ho spiegato le differenze tra questi due approcci, e perchè bisognerebbe sempre, ove possibile, favorire la composizione.
Il pattern strategy è un ottimo esempio di utilizzo della composizioni a favore dell’ereditarietà.
Il diagramma seguente ci mostra come con l’ereditarietà possiamo ottenere lo stesso risultato:
Questo tipo di approccio può portare:
- Alla generazione di gerarchie di classi mostruose
- Impossibilità di modificare l’algoritmo dinamicamente, l’ereditarietà è statica, stabilita a compile-time
Inoltre quello che otterremo sarà una famiglia di classi la cui unica differenza consiste nell’algoritmo che ognuna di esse implementa diversamente.
E se provassimo a separare le parti soggette a variare in modo che gli effetti delle variazioni non si propaghino anche alle parti che possono restare fisse?
Context
delegaStrategy
all’esecuzione dell’operazione (algoritmo).- L’algoritmo viene fatto secondo la strategia concreta istanziata.
- A tempo di esecuzione si può sostituire un algoritmo con un altro.
Conclusioni
Come con tutti i migliori modelli, il pattern strategy oltre ad essere potente è anche semplice. Quando le classi devono supportare più implementazioni di un comportamento, l’approccio migliore è spesso di estrarre queste implementazioni e metterle in un loro tipo, piuttosto che di estendere la classe originale per gestirle.
Ricordate il principio della Banda dei Quattro: “favorire la composizione per l’eredità“? Questo è un ottimo esempio. Definendo ed incapsulando gli algoritmi, riduciamo le sottoclassi e aumentiamo la flessibilità.
È possibile aggiungere nuove strategie in qualsiasi momento senza la necessità di cambiare Context.
Quindi il pattern incapsula un insieme di algoritmi e li rende intercambiabili, permettendo a questi di variare indipendentemente dagli oggetti che li utilizzano:
- Incapsula ciò che cambia e lascia quello che non cambia.
- Rende gli algoritmi incapsulati intercambiabili.
- Favorisce la composizione per l’eredità.