Strategy Design Pattern

S

Uno dei Design Pattern più semplici e dalla indiscussa utilità è lo Strategy Pattern.

Pattern inizialmente definito dalla Gang of Four, consente di isolare un algoritmo al di fuori di una classe, per far sì che quest’ultima possa variare dinamicamente il suo comportamento, rendendo così gli algoritmi intercambiabili a runtime.

Prendendo in esame una famiglia di algoritmi (es. algoritmi di ordinamento, di crittografia, di validazione, etc) grazie allo Strategy Pattern è possibile utilizzare una qualsiasi implementazione (che viene genericamente chiamata Strategy o Strategia), scegliendo fra quelle disponibili, che si rende più opportuna in un determinato contesto, in quanto tutte le implementazioni facenti parte della stessa famiglia hanno interfaccia comune.

MOTIVAZIONI

Quando una classe “Context” esegue delle operazioni che richiedono l’implementazione di un algoritmo è facile pensare di includere l’algoritmo direttamente nella definizione dell’oggetto che ne dovrà fare utilizzo.

Tuttavia, gli algoritmi necessari allo svolgimento di una determinata operazione potrebbero variare nel tempo, rendendo necessaria la modifica della classe “Context” (o in alternativa, una sua sottoclasse che esegue l’overload dei metodi necessari ad implementare la funzionalità in oggetto).
Se immaginiamo una collezione di dati che espone un metodo per l’ordinamento, alcuni degli scenari più comuni, che ci potrebbero portare a variare l’algoritmo di sort sono i seguenti:

  • L’apporto di alcune migliorie negli algoritmi (o la correzione di bug),
  • Un cambiamento del contesto ambientale nel quale le nostre classi vengono utilizzate (vincoli di spazio o di tempo)

Inoltre, un simile approccio, fa inevitabilmente crescere la complessità della classe “Context” che dovrà contenere sia le logiche necessarie alla gestione della collezione di dati, che le logiche necessarie al suo ordinamento.

È facile a questo punto capire che una simile implementazione non è frutto di un buon design in quanto poco manutenibile/scalabile. In particolare, viola due dei principi SOLID, ovvero il principio di singola responsabilità (ogni classe dovrebbe avere una ed una sola responsabilità, interamente incapsulata al suo interno) e il principio di Aperto/Chiuso (un’entità dovrebbe essere aperta alle estensioni, ma chiusa alle modifiche).

Ci viene in aiuto lo Stategy Pattern.

Come funziona

Questo Design Pattern consiste nell’incapsulare un algoritmo all’interno di una classe, mantenendo un’interfaccia generica. Il tutto si traduce nel seguente diagramma delle classi.

Più nel dettaglio:

  • Strategy: dichiara l’interfaccia della nostra classe di algoritmi. Interfaccia che viene utilizzata da Context per invocare un algoritmo concreto
  • ConcreteStrategy (A e B) sono i nostri algoritmi concreti, ovvero implementano uno specifico algoritmo che espone l’interfaccia Strategy
  • Context, classe di contesto che invoca la ConcreteStrategy (sotto richiesta dei suoi client). Può esporre un’interfaccia, per consentire “all’algoritmo” di accedere ad una sua eventuale struttura dati interna.

Per rifarci al precedente esempio (la collezione di oggetti):

  • Context è la collezione stessa
  • Le strategie concrete potrebbero essere ad esepio BubbleSort, QuickSort, MergeSort, etc
  • Strategy sarà l’interfaccia comune a tutti i precedenti algoritmi di ordinamento.

Un esempio pratico

Cerchiamo di fare chiarezza con un semplice esempio: l’ordinamento di una collezione di dati.

Innanzitutto definiamo l’interfaccia SortStrategy, del nostro algoritmo, ovvero un unico metodo che si occuperà di eseguire il sort (e riceve come parametro l’array di elementi per riferimento).

interface SortStrategy
{
    function sort(array &$elements);
}
interface SortStrategy
{
    void sort(ref int[] elements);
}

Procediamo quindi con la definizione di una classe per incapsulare un semplice algoritmo di ordinamento. In questo caso ho deciso di usare un bubble sort, andando quindi a definire la classe BubbleSortStrategy.

class BubbleSortStrategy implements SortStrategy
{
    public function sort(array &$elements) {
        $size = count($elements);
        do {
            $swapped = false;
            for ($i = 0; $i < $size - 1; $i++) {
                if ($elements[$i] > $elements[$i + 1]) {
                    $this->swap($elements[$i], $elements[$i + 1]);
                    $swapped = true;
                }
            }
            $size--;
        } while ($swapped);
    }
    
    protected function swap(&$a, &$b) {
        $tmp = $a;
        $a = $b;
        $b = $tmp;
    }
}
class BubbleSortStrategy : SortStrategy
{
    public void sort(ref int[] elements) {
        var size = elements.Length;
        bool swapped;
        do {
            swapped = false;
            for (int i = 0; i < size - 1; i++) {
                if (elements[i] > elements[i + 1]) {
                    this.swap(ref elements[i], ref elements[i + 1]);
                    swapped = true;
                }
            }
            size--;
        } while (swapped);
    }

    protected void swap(ref int a, ref int b) {
        var tmp = a;
        a = b;
        b = tmp;
    }
}

In questo contesto BubbleSortStrategy potrebbe anche contenere dei parametri di istanza (ad esempio un flag che stabilisce se l’ordinamento è ascendente o discendente). Per questo motivo non è difficile trovare lo Strategy Pattern in accoppiata con Factory Method o Abstract Factory.

Infine definiamo la nostra collezione ovvero la classe di contesto che dovrà utilizzare l’algoritmo di ordinamento (per brevità il codice di esempio implementa solo i metodi utili all’utilizzo dello Strategy Pattern).

class Collection
{
    protected $elements;
    protected $sortStrategy;

    public function __construct(array $elements) {
        $this->elements = $elements;
    }

    public function setSortStrategy(SortStrategy $sortStrategy) {
        $this->sortStrategy = $sortStrategy;
    }

    public function sort() {
        if($this->sortStrategy) {
            $this->sortStrategy->sort($this->elements);
        }
    }

    public function __toString() {
        return '['.implode(', ', $this->elements).']';
    }
    
    //...
}
class Collection
{
    protected int[] elements;
    protected SortStrategy sortStrategy;

    public Collection(int[] elements) {
        this.elements = elements;
        this.sortStrategy = null;
    }

    public void setSortStrategy(SortStrategy sortStrategy) {
        this.sortStrategy = sortStrategy;
    }

    public void sort()
    {
        if (this.sortStrategy != null) {
            this.sortStrategy.sort(ref this.elements);
        }
    }

    public override String ToString()
    {
        return "[" + string.Join(
            ", ", 
            this.elements.Select(
                item => item.ToString()
            ).ToArray()
        ) + "]";
    }

    //...
}

La classe definisce due metodi per l’utilizzo dello strategy:

  • setSortStrategy, che consente di settare/modificare la strategia di sorting
  • sort, che invoca la strategia (quando settata), con i dovuti parametri

A questo punto possiamo testare il risultato:

$collection = new Collection(array(1, 7, 6, 5, 4, 3, 2));
$collection->setSortStrategy(new BubbleSortStrategy());
$collection->sort();
echo $collection."\n";
var collection = new Collection(new int[]{ 1, 7, 6, 5, 4, 3, 2 });
collection.setSortStrategy(new BubbleSortStrategy());
collection.sort();
Console.WriteLine(collection);
Console.ReadKey();

Il codice di esempio dovrebbe essere abbastanza eloquente:

  • La prima riga crea un’istanza della collezione, con dei valori di default
  • L’invocazione del metodo setSortStrategy ci consente di settare una strategia di ordinamento (in questo caso l’istanza di BubbleSortStrategy)
  • L’invocazione del metodo sort ordina gli elementi presenti nella collezione (andando a sua volta ad invocare il metodo sort della strategia di ordinamento)

Potete provare voi stessi.

Conclusioni

Abbiamo visto come implementare il pattern Strategy tramite un esempio concreto.

È facile notare che l’utilizzo di Strategy è spesso un’alternativa all’ereditarietà perché ci consente di variare il comportamento di una classe, senza doverne eseguire un’overriding, rendendo più lineare il nostro progetto software. D’altro canto, tutti i client che utilizzano la nostra classe “Context” devono conoscere lo Strategy più indicato da utilizzare. In seconda battuta la definizione dell’interfaccia delle Strategie e del Context potrebbe non essere così banale (per via delle interazioni necessarie allo Strategy per espletare un determinato compito).

A mio avviso Strategy è uno di quei pattern che proprio non si può fare a meno di conoscere, nonché utilissimo nei più svariati contesti.

A proposito di me

Paolo Rizzello

Nato e cresciuto in un'azienda di informatica, ha (inevitabilmente) intrapreso la strada dell'IT con un diploma da perito informatico prima e una laurea in ingegneria informatica poi, facendo, della sua passione, un lavoro a tutti gli effetti.
Si guadagna da vivere sviluppando web application in Laravel e ASP.NET... ma ha un oscuro passato di sviluppatore C++.

I nostri Partner

Gli articoli più letti

Articoli recenti

Commenti recenti