Strategy

Il Design Pattern Strategy è un pattern comportamentale il cui obiettivo è quello di isolare un algoritmo, o una famiglia di algoritmi, all’interno di un oggetto. Il pattern strategy è utile nelle situazioni in cui si rende necessario modificare dinamicamente gli algoritmi utilizzati dall’applicazione in un certo contesto. L’utilizzo di questo pattern consente l’implementazione di algoritmi tra loro intercambiabili (in base ad una qualche condizione) in modo trasparente al client che ne fa uso.

Si consideri ad esempio un sistema OO incaricato di descrivere le caratteristiche di un gruppo di anatre, all’interno di questo sistema le diverse tipologie di anatre esistenti deriveranno dalla classe Duck, la quale conterrà alcuni metodi utili, tra questi:

  • quack(), che riproduce il verso dell’animale;
  • swim(), che descrive il modo di nuotare dell’animale;
  • display(), che descrive l’aspetto dell’animale.

La classe Duck è una classe astratta contenente la descrizione generale di tutte le anatre, essa conterrà tutte le azioni che le anatre devono poter compiere per essere definite tali, in questo caso emettere un suono (il metodo quack()) e nuotare (il metodo swim()). La classe contiene inoltre un metodo astratto (il metodo display()) in grado di rappresentare esteticamente l’anatra in questione. Tutte le anatre sono in grado di emettere un certo verso e di nuotare, tuttavia le diverse anatre assumono un aspetto differente a seconda della loro razza, per questo motivo il metodo display() viene di volta in volta riscritto utilizzando le caratteristiche più opportune.

Supponiamo di modificare in un secondo momento l’implementazione della classe Duck, inserendovi all’interno delle nuove caratteristiche che dovranno essere adottate da tutte le classi che la erediteranno. La modifica alla classe Duck prevede, ad esempio, l’introduzione di un nuovo metodo in grado di descrive la capacità delle anatre di volare (il metodo fly()).

Supponiamo inoltre, che nel lasso di tempo trascorso tra la prima implementazione e la successivamente modifica, un membro del nostro team abbia deciso di definire una sottoclasse denominata RubberDuck, incaricata di descrivere il comportamento delle paperelle di gomma. Se è vero che le papere di gomma possono emettere versi (ad esempio se strizzate) o nuotare (esse infatti galleggiano), esse non saranno sicuramente in grado di volare.

Progettando utilizzando alcuni tra i più comuni linguaggi di programmazione, tra cui il Java, non avremo la possibilità di impedire ad una specifica sottoclasse di ereditare i metodi forniti dalla classe padre, in questo caso saremo quindi obbligati a riscrivere il comportamento del metodo fly(), magari impedendo il funzionamento di tale caratteristica.

Le possibili soluzioni a nostra disposizione sono:

  • Override (come da esempio), eseguire un override del metodo all’interno della classe riservata alle paperelle di plastica, creandone uno vuoto oppure uno lanciando un eccezione. Questo sistema non è comodo, poiché esso impone una verifica dei metodi caso per caso;
  • Interfacce, creare diverse interfacce riservate ai diversi metodi e poi estenderle all’interno delle classi che le richiedono. Questo approccio ha un difetto alla radice, non permette di fattorizzare il codice;
  • Delegation, utilizzate il pattern Strategy.

A differenza del pattern singleton, il cui scopo è quello identificare gli aspetti dell’applicazione che variano, al fine di separarli da ciò che è sempre uguale (l’obiettivo del pattern è quello di definire una famiglia di algoritmi in modo da renderli intercambiabili), il pattern strategy propone un approccio più orientato alle interfacce e meno all’implementazione.

Nell’esempio precedente le diverse anatre si differenziano sulla base delle loro capacità di volare, tuttavia esse potrebbero anche differenziarsi nelle modalità di emissioni del verso “quack”. Applicando il pattern strategy occorre procedere inizialmente con l’identificazione degli elementi che variano, ai quali viene fornita identità di classe per mezzo di un’interfaccia.

Successivamente si procede con l’inserimento di queste interfacce all’interno della classe padre, tale inserimento non avverrà mediante un operazione di implementazione di interfaccia, ma mediante l’inserimento di alcuni attributi nella classe stessa (relazione di aggregazione).

La classe Duck non andrà quindi ad indicare se una particolare anatra (da lei discendente) saprà (o non saprà volare), ma imporrà alle sue classi figlie l’obbligo di dichiarare il loro comportamento. L’utilizzo di questo pattern permette un’ampia apertura al cambiamento, in ogni momento sarà possibile aggiungere una nuova classe (e quindi un nuovo metodo) ad una certa interfaccia, specificando di fatto un nuovo comportamento rispetto ad una certa caratteristica.

Definizione

Il pattern stategy permette di definire una famiglia di algoritmi, isolarli uno ad uno, e successivamente renderli intercambiabili. Nell’esempio precedente abbiamo definito in modo isolato i diversi comportamenti dell’oggetto rispetto alla famiglia di algoritmi di volo, e successivamente gli abbiamo resi intercambiabili nelle diverse classi.

All’interno del diagramma UML la classe Context rappresenta la classe che necessita di implementare le diverse interfacce, nell’esempio precedente questa era rappresentata dalla classe Duck. L’interfaccia Strategy è l’interfaccia incaricata di raggruppare i diversi algoritmi, questa definisce un certo metodo astratto che sarà poi implementato della classi ConcreteStrategy. Nell’esempio precedente le classi ConcreteStrategy erano rappresentate dalle diverse implementazioni del metodo fly() e del metodo quack(). La classe Context e le interfacce Strategy sono in relazione di composizione.

Strategy in Java

Viene di seguito presentato un esempio applicativo scritto in linguaggio Java:

/* Strategy */
public interface FlyBehavior {
  public void fly();
}

/* ConcreteStrategy */
public  class CanFly implements FlyBehavior {
  public void fly() {
    canFly();
  }
  private void canFly() {
    // vola!
  }
}

/* ConcreteStrategy */
public  class CantFly implements FlyBehavior {
  public void fly() {
    cantFly();
  }
  private void cantFly() {
    // non vola!
  }
}

public class Duck {
  private FlyBehavior strategy;
  public Duck() {
    /* */
  }
  public void setSortStrategy(quackStrategy strategy) {
    this.strategy = strategy;
  }
  public void fly() {
    strategy.fly();
  }
}