PHP – Design Pattern Strategy

22 Apr

Out Of Date Warning

Questo post è stato pubblicato più di 2 anni fa (il 22 aprile 2013). Le idee vanno avanti velocemente, le prospettive cambiano quindi i contenuti potrebbero non essere aggiornati. Ti prego di tenere in considerazione questo, e di verificare le informazioni tecniche presenti nell'articolo prima di farne affidamento per i tuoi scopi.

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 un ConcreteStrategy.
  • ConcreteStrategy: Classe concreta, che implementerà un algoritmo usando l’interfaccia Strategy.
  • Contex: Contiene un oggetto ConcreteStrategy e utilizza un riferimento a Strategy per eseguire l’algoritmo concreto.

Struttura

strategy pattern diagram
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;

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 di IStrategy (anch’esso chiamato algorithm()).

Strategy

L’interfaccia IStrategy prevede l’implementazione di un unico metodo 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:

Client

Le seguenti due linee di codice sono sufficientemente autoesplicative su come il client utilizzerà il pattern:

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.

Il pattern Strategy impiega i principi del poliformismo al fine di prevenire l’utilizzo di istruzioni condizionali ricorrenti nel codice.

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:

Nota: l’interfaccia Strategy dovrà essere definita in modo tale da poter supportare qualsiasi algoritmo.

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.

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:
ereditarietaQuesto 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?
composizione

  • Context delega Strategy 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à.

Lascia un commento