Strategy Design Pattern

Sicuramente uno dei design pattern dalla indiscussa utilità ed il cui utilizzo può rendere il nostro codice  molto più chiaro, leggibile e semplice da manutenere. Da un punto di vista formale la Gang of Four definisce lo Strategy Pattern come  un pattern che consente di isolare un insieme di algoritmi all’esterno della classe che li utilizza, per fare si che quest’ultima li possa utilizzare in modo intercambiabile, rendendo dinamico il proprio comportamento .

Prima di introdurre formalmente la struttura delle classi che implementa il pattern, proviamo a comprendere il significato reale di tali parole procedendo con un semplice esempio che risolveremo senza l’utilizzo del pattern strategy, ed evidenziandone le limitazioni, per poi vedere come il pattern le supera elegantemente.

 Per chi non sia interessato può andare direttamente al paragrafo Definizione Formale.

Esempio di Shop Online

Cerchiamo di comprendere le motivazioni che spingono all’utilizzo dello strategy pattern tentando di risolvere il seguente semplice problema.  Supponiamo di voler offrire ai clienti di uno shop online diversi sistemi di pagamento, da scegliere al momento del check-out.  Il primo semplice approccio al problema può essere quello di realizzare una classe astratta che implementa i comportamenti comuni e più sottoclassi concrete che realizzano i comportamenti specifici.

Definiamo quindi la classe astratta AbstractPaymentProcessor e la classe CheckOut the la utilizza nel modo seguente:

I metodi pay() e refund() sono astratti, e devono quindi essere definiti nelle sottoclassi che implementano la classe astratta. Supponiamo ora di voler offrire i metodi di pagamento via Carta di Credito e via PayPal.  La sola differenza è che con la carta di credito è possibile eseguire entrambe le operazioni, mentre con PayPal l’operazione di refund() non è ammessa. Le possibili implementazioni di tali “strategie” sono:

Si noti che per implementare il vincolo su PayPal l’operazione di refund() solleva una eccezione. Supponiamo ora che ci venga richiesto di introdurre un altro vincolo ma solamente per le carte di tipo VISA, per le quali la refund() non è ammessa. Procedendo con lo stesso stile introduciamo la classe VisaPaymentProcessor che estende CreditCardPaymentProcessor sovrascrivendo il metodo refund() nel modo seguente:

Ma i nostri problemi non finiscono qui, perchè ad un certo punto il cliente ci chiede di implementare anche il metodo di pagamento attraverso IBAN, il quale però oltre a supportare tutte le operazioni di pagamento e rimborso, consente anche di eseguire un rimborso parziale. Al nostro cliente interessa moltissimo tale opzione e decide di offrirla ai visitatori dello shop online. Per mantenere la struttura del nostro codice consistente siamo quindi costretti ad introdurre il metodo astratto partialRefund() nella classe astratta AbstractPaymentProcessor, il quale dovrà essere implementato in tutte le sottoclassi sollevando un’eccezione tranne nella nuova classe IbanPaymentProcessor così definita:

La figura seguente mostra l’architettura delle classi fino ad ora realizzare per risolvere il nostro problema:

Come è evidente dall’immagine, la struttura della soluzione è molto complessa ed è destinata a complicarsi ancora di più ad ogni richiesta evolutiva del cliente. Basti pensare a cosa succederebbe se avessimo non 3 ma decine di metodi di pagamento differenti. Ad ogni richiesta dovremmo mettere mano a diverse righe di codice già testato e funzionante.

Utilizzo dello Strategy Pattern

Prima di procedere introducendo il pattern, procediamo per gradi ricordando uno dei principi fondamentali della progettazione software e dell’incapsulamento, ovvero:

Identificare le parti del programma che variano, mantenendole separate da ciò che rimane sempre lo stesso.

In parole semplici tale principio ci dice che dobbiamo separare ed incapsulate tutto ciò che varia frequentemente, così da gestire tutto il codice associato in un unico punto del programma. In questo modo ogni cambiamento a tale codice non ha effetto sul resto del programma, che risulta quindi più flessibile e robusto.

Applicando tale principio al nostro esempio è evidente che il comportamento dei metodi pay(), refund() e partialRefund() sono candidati ideali per poter essere incapsulati al di fuori della classe principale. Questi comportamenti infatti variano tra i diversi metodi di pagamento e possono essere spostati in classi separate.

Introduciamo quindi tre interfacce PayStrategy, RefundStrategyPartialRefundStrategy che sono definite nel seguente modo:

Tali interfacce sono poi implementate da tante classi quanti sono i metodi di pagamento. Non riportiamo per semplicità la loro implementazione, ci basti sapere che, per il nostro esempio, si tratta di 8 classi, ognuna delle quali costituita da poche righe di codice. Le classi implementate sono mostrate in figura:

Prima di procedere all’utilizzo di tali interfacce e delle classi che le implementano, scomodiamo un’altro principio cardine della progettazione software:

Programmare per interfacce, non per implementazione.

Tale principio ci suggerisce di evitare il più possibile l’utilizzo di classi concrete, ma di preferire piuttosto classi astratte o interfacce che definiscono tali classi.

Applicato al nostro caso, significa implementare una classe PaymentProcessor che realizza le operazioni di pagamento e rimborso senza però conoscere a priori con quale metodo di pagamento vengono eseguite. Dal punto di vista pratico significa che la classe opera su oggetti che implementano le interfacce PayStrategy, RefundStrategyPartialRefundStrategy. Oggetti che vengono iniettate attraverso specifici metodi set() al momento dell’effettivo utilizzo della classe, la quali quindi non conosce a priori su quali oggetti opererà in modo concreto.

Una interessante conseguenza di tale approccio è che è possibile implementare nuove strategie di pagamento e rimborso, al di fuori di quelle viste, semplicemente mescolando i diversi comportamenti e senza dover implementare nuove classi. Questo implica, ad esempio, che è possibile definire un oggetto PaymentProcessor che esegue i pagamenti attraverso PayPal, il rimborso mediante Carta di Credito e il rimborso parziale con l’IBAN.

La classe CheckOut riportata di seguito mostra come, ad esempio, sia possibile implementare il metodo di pagamento mediante carta di credito di tipo VISA, senza dover introdurre una nuova classe, ma semplicemente mixando i comportamenti già disponibili:

Per completezza riportiamo il diagramma delle classi parziale che si riferisce a quest’ultimo esempio:

Definizione Formale

Il pattern Strategy è un pattern comportamentali che può essere utilizzato ogni qual volta una classe Context esegue delle operazioni sui dati che richiedono l’implementazione di un algoritmo che può variare nel tempo. L’adozione di tale pattern evita la necessità di dover modificare la classe Context ad ogni nuova implementazione dell’algoritmo.

Nel linguaggio java un buon esempio dell’utilizzo di tale pattern lo troviamo nel metodo Collection.sort(), che riceve in input, oltre alla collezione da ordinare, un oggetto che implementa l’interfaccia Comparator:

La soluzione che il pattern ci propone è quindi di incapsulare un algoritmo all’interno di una classe esterna alla classe Context, definendo una interfaccia generica. Il tutto si traduce nel seguente diagramma delle classi:

Dove:

  • Strategy: dichiara l’interfaccia della classe di algoritmi. Interfaccia che è utilizzata dalla classe Context per invocare uno specifico algoritmo concreto.
  • ConcreteStrategyA e ConcreteStrategyB: sono le classi che incapsulano gli algoritmi concreti, ovvero implementano uno specifico algoritmo esponendo l’interfaccia Strategy.
  • Context: è la classe di contesto che invoca la ConcreteStrategy (sotto richiesta dei suoi client).

Codie Sorgente

Il codice sorgente del progetto è disponibile qui strategy.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

*