Home > GOF Pattern, java > GoF Patterns: State

GoF Patterns: State

13 marzo 2012

Translate in English with Google Translate
In questo articolo tratterò il pattern State anche detto Objects for States

Motivazione
Si tratta di un pattern comportamentale basato su oggetti che viene utilizzato quando il comportamento di un oggetto deve cambiare in base al suo stato. Questo pattern trova applicazione quando abbiamo a che fare con una “Macchina a Stati Finiti” ossia siamo in presenza di un sistema dinamico in cui i valori di ingresso, uscita e stato sono un insieme finito.

Solitamente il codice è pieno di istruzioni condizionali che esaminano lo stato degli oggetti, in particolare esaminano il valore di variabili e costanti al fine di prendere delle decisioni. Molto spesso queste decisioni sono molto complesse e dipendono da molti valori, tutto ciò determina la generazione di grossi blocchi IF-ELSE/SWITCH che spesso contengono anche logica di business. Questo comporta seri problemi di comprensione, manutenzione ed evoluzione del codice.

L’utilizzo di questo pattern permette di scorporare i grossi blocchi condizionali ed inserirli negli oggetti di stato, in modo da associare il comportamento di un oggetto al suo stato. Un linguaggio ad oggetti ha delle caratteristiche basate sul Paradigma Orientato agli Oggetti che consentono di creare oggetti con un proprio stato ed un proprio comportamento e l’utilizzo di questo pattern consente di sfruttare queste caratteristiche.

Partecipanti e Struttura
Questo pattern è composto dai seguenti partecipanti:

  • Context: definisce l’interfaccia di interesse del Client e mantiene una instanza della ConcreteState che definisce lo stato corrente
  • State: definisce un interfaccia per incapsulare il comportamento associato con un particolare stato.
  • ConcreteState: ogni sub-classe che implemento un comportamento associato con uno stato.

Vediamo come si presenta il Pattern State utilizzando il Class Diagram in UML:

Conseguenze
Tale pattern presenta i seguenti vantaggi/svantaggi:

  • specializza il comportamento associato ad uno stato: per ogni stato vengono definiti i rispettivi comportamenti pertanto nuovi stati possono specializzare gli stati preesistenti. Questo evita di creare dei grossi blocchi decisionali e consente di inserire, negli oggetti di stato, i blocchi decisionali di pertinenza, in questo modo favorendo la comprensione e manutenzione del codice. Un inconveniente deriva dal fatto che vengono a crearsi molti oggetti di stato e le desioni non sono centralizzate ma sono distribuite sugli stati.
  • rende esplicita la transizione di stato: il passaggio da uno stato ad un altro dipende dal verificarsi di una condizione esplicita che viene dichiarata esplicitamente.
  • condivisione di oggetti di stato: se l’oggetto di  stato non ha variabili di instanza ma solo comportamenti, può essere condiviso con altri oggetti.

Implementazione

Anche questo pattern si presta per molti esempi pertanto ho pensato di proporre 2 esempi, il primo più semplice per mostrare il codice minimale ed il secondo esempio più complesso ma più pratico:

  1. selezionare il colore di una Palette
  2. seguire i passaggi di stato di un ordine di acquisto

Il primo esempio propone la selezione del colore di una Palette, infatti usando uno strumento di painting possiamo selezionare il colore da utilizzare per disegnare. Il colore è uno stato della Palette ed indica il colore attualmente selezionato per il disegno. La Palette ha una proprietà di stato relativa al colore e questo comporta avere una referenza con un oggetto Color che a seconda del colore selezionato potrà essere rosso, giallo ecc.

Vediamo come si presenta il pattern in UML in base all’esempio:

Creiamo la classe Palette che detiene la referenza di un oggetto Color e presenta dei metodi che consentono di effettuare il set ed il get del colore.

package patterns.state;

public class Palette {

    private Color color;

    public void setColor(Color color) {
        this.color=color;
    }

    public void currentColor() {
        color.showColor();
    }

}

L’interfaccia Color dichiara il metodo showColor() che dovrà essere implementato dalle classi concrete.

package patterns.state;

public interface Color {

    public void showColor();

}

La classe WhiteColor implementa l’interfaccia definendo le caratteristiche del colore, in questo caso ho definito 2 proprietà del colore sotto forma di stringa ed di valore esadecimale.

package patterns.state;

public class WhiteColor implements Color{

    private static final String COLOR_NAME = "WHITE";
    private static final String HEX_COLOR = "#FFFFFF";

    @Override
    public void showColor() {
        System.out.println( COLOR_NAME );
    }

}

Idem per la classe RedColor

package patterns.state;

public class RedColor implements Color{

    private static final String COLOR_NAME = "RED";
    private static final String HEX_COLOR = "#FF0000";

    @Override
    public void showColor() {
        System.out.println( COLOR_NAME );
    }

}

Il Client può selezionare a piacimento il colore dalla Palette.

package patterns.state;

public class Client {

    public static void main(String[] args) {
        Palette context = new Palette();
        context.setColor(new RedColor());
        context.currentColor();
        context.setColor(new WhiteColor());
        context.currentColor();
    }
}

Eseguiamo l’esempio e notiamo come lo stato della Palette cambi in base all’ultimo colore selezionato che indica lo stato della Palette

$JAVA_HOME/bin/java patterns.state.Client
RED
WHITE

Come secondo esempio vediamo un caso più complesso ma probabilmente più pratico ed utile: gli stati di un ordine di acquisto. Consideriamo un bookshop e ipotizziamo una lista di stati dell’ordine:

  • Nuovo: ordine ricevuto
  • In Prenotazione: libro ancora non pubblicato in commercio
  • In Corso: reperimento del libro in corso
  • Difettoso: libro recuperato ma con difetto. Verrà recuperata una nuova copia
  • Esaurito: libro non più disponibile, ordine da cancellare
  • Pronto: libro recuperare da spedire a cliente
  • Cancellato: ordine cancellato
  • In Partenza: spedizione in corso.
  • Spedito: ordine spedito al cliente

Gli stati elencati, puramente indicativi, sono visualizzati come dei rettangoli smussati (State) e collegati tra di loro tramite delle frecce ( Transition ) su cui sono indicate le condizioni ( Guard ) che determinano il passaggio di stato, i rombi (Choice) rappresentano delle situazioni di scelta mutuamente esclusive, i punti di inizio ( Inizial ) e fine ( Final ) rappresentano il punto di entrata e di uscita del diagramma.

Vediamo lo State Diagram in UML:

In base agli stati dell’ordine possiamo definire in UML un Class Diagram che ci mostri una soluzione SENZA UTILIZZARE lo State Pattern. In questo caso gli stati sono definiti tramite degli oggetti String e la transizione delle fasi è gestita da un grosso blocco condizionale IF-ELSE.

Vediamo la classe Ordine che detiene lo stato:

package patterns.no.state;

public class Ordine {

    private String statoOrdine;

    public Ordine() {
        this.statoOrdine = Stato.NUOVO;
    }

    public String getStatoOrdine() {
        return statoOrdine;
    }

    public void setStatoOrdine(String statoOrdine){
        this.statoOrdine = statoOrdine;
    }

}

La classe Stato definisce gli stati dell’ordine. Questa classe contiene una lista di oggetti String. Si tratta di un approccio molto usato per definire una serie di elementi definito “String Enum Pattern” e citato da Josha Bloch in “Effective Java” come tecnica da evitare a fronte degli enumerated type.

package patterns.no.state;

public class Stato {

    public static final String NUOVO = "NUOVO";
    public static final String IN_PRENOTAZIONE = "IN_PRENOTAZIONE";
    public static final String IN_CORSO = "IN_CORSO";
    public static final String DIFETTOSO = "DIFETTOSO";
    public static final String PRONTO = "PRONTO";
    public static final String ESAURITO = "ESAURITO";
    public static final String CANCELLATO = "CANCELLATO";
    public static final String IN_PARTENZA = "IN_PARTENZA";
    public static final String SPEDITO = "SPEDITO";

}

La classe GestoreOrdine avvia l’esempio e simula gli step di un ordine a buon fine:
Nuovo->In Corso->Pronto->In Partenza->Spedito
Il metodo gestioneStatoOrdine() si occupa di gestire lo stato dell’ordine tramite un grosso blocco IF-ELSE che sicuramente non desterebbe le simpatie dei promotori della “Campagna Anti-IF

package patterns.no.state;

public class GestoreOrdine {

    public static void main(String[] args) {
        new GestoreOrdine().esecuzioneOrdine();
    }

    public void esecuzioneOrdine() {
        Ordine ordine = new Ordine();

        String statoOrdine = ordine.getStatoOrdine();
        System.out.println("Stato attuale dell'ordine: " + statoOrdine);

        gestioneStatoOrdine(ordine, "in_corso");
        statoOrdine = ordine.getStatoOrdine();
        System.out.println("Stato attuale dell'ordine: " + statoOrdine);

        gestioneStatoOrdine(ordine, "pronto");
        statoOrdine = ordine.getStatoOrdine();
        System.out.println("Stato attuale dell'ordine: " + statoOrdine);

        gestioneStatoOrdine(ordine, "in_partenza");
        statoOrdine = ordine.getStatoOrdine();
        System.out.println("Stato attuale dell'ordine: " + statoOrdine);

        gestioneStatoOrdine(ordine, "spedito");
        statoOrdine = ordine.getStatoOrdine();
        System.out.println("Stato attuale dell'ordine: " + statoOrdine);
    }

    public void gestioneStatoOrdine(Ordine ordine, String stato) {
        if (stato.equals("in_prenotazione")) {
            ordine.setStatoOrdine(Stato.IN_PRENOTAZIONE );
        } else if (stato.equals("in_corso")) {
            ordine.setStatoOrdine(Stato.IN_CORSO );
        } else if (stato.equals("difettoso")) {
            ordine.setStatoOrdine(Stato.DIFETTOSO );
        } else if (stato.equals("pronto")) {
            ordine.setStatoOrdine(Stato.PRONTO );
        } else if (stato.equals("esaurito")) {
            ordine.setStatoOrdine(Stato.ESAURITO );
        } else if (stato.equals("cancellato")) {
            ordine.setStatoOrdine(Stato.CANCELLATO );
        } else if (stato.equals("in_partenza")) {
            ordine.setStatoOrdine(Stato.IN_PARTENZA );
        } else if (stato.equals("spedito")) {
            ordine.setStatoOrdine(Stato.SPEDITO );
        }
    }

}

Eseguiamo il GestoreOrdine per visualizzare la simulazione degli stati dell’ordine.

$JAVA_HOME/bin/java patterns.no.state.GestoreOrdine
Stato attuale dell'ordine: STATO_NUOVO
Stato attuale dell'ordine: STATO_IN_CORSO
Stato attuale dell'ordine: STATO_PRONTO
Stato attuale dell'ordine: STATO_IN_PARTENZA
Stato attuale dell'ordine: STATO_SPEDITO

Adesso UTILIZZIAMO lo State Pattern per il nostro esempio ed evitiamo l’uso della tecnica “String Enum Pattern” ed i  grossi blocchi IF-ELSE come proposto dalla “Campagna Anti-IF”.
L’uso di questo pattern non elimina i costrutti decisionali ma li incorpora negli stati in modo da evitare dei monolitici blocchi IF-ELSE. Ogni stato gestisce le transizioni sulla base di condizioni esprimibili con le istruzione condizionali.

Vediamo come possiamo effettuare il refactoring sulla base di queste informazioni e rappresentiamo il nostro Class Diagram in UML:

La classe Ordine non presenta grosse modifiche ad esclusione del fatto che adesso lo stato è detenuto da un oggetto di tipo State e non di tipo String come avveniva precedentemente:

package patterns.state;

public class Ordine {

    private Stato statoOrdine;

    public Ordine(){
        this.statoOrdine = new StatoNuovo();
    }

    public Stato getStatoOrdine() {
        return statoOrdine;
    }

    public void setStatoOrdine(Stato statoOrdine){
        this.statoOrdine = statoOrdine;
    }

}

L’interfaccia State definisce il metodo di gestione dello stato, pertanto le logiche di transizione di stato non vengono gestite in modo centralizzato ma in modo distribuito dai singoli oggetti di stato.

package patterns.state;

public interface Stato {

    public void gestioneStatoOrdine(Ordine ordine, String stato);
}

Lo stato StatoNuovo gestisce le transizioni di stato verso lo stato StatoInPrenotazione oppure StatoInCorso come previsto dallo State Diagram.

package patterns.state;

public class StatoNuovo implements Stato {

    @Override
    public void gestioneStatoOrdine(Ordine ordine, String stato) {
        if (stato.equals("in_prenotazione"))
           ordine.setStatoOrdine(new StatoInPrenotazione());
        else if (stato.equals("in_corso"))
           ordine.setStatoOrdine(new StatoInCorso());
    }

}

Lo stato StatoInPrenotazione gestisce le transizioni di stato verso lo stato StatoInCorso come previsto dallo State Diagram.

package patterns.state;

public class StatoInPrenotazione implements Stato {

    @Override
    public void gestioneStatoOrdine(Ordine ordine, String stato) {
        if (stato.equals("in_corso"))
           ordine.setStatoOrdine(new StatoInCorso());
    }

}

Lo stato StatoInCorso gestisce le transizioni di stato verso lo stato StatoEsaurito, StatoCancellato, StatoPronto e StatoDifettoso come previsto dallo State Diagram.

package patterns.state;

public class StatoInCorso implements Stato {

    @Override
    public void gestioneStatoOrdine(Ordine ordine, String stato) {
        if (stato.equals("esaurito")) {
            ordine.setStatoOrdine(new StatoEsaurito());
        } else if (stato.equals("cancellato")) {
            ordine.setStatoOrdine(new StatoCancellato());
        } else if (stato.equals("pronto")) {
            ordine.setStatoOrdine(new StatoPronto());
        } else if (stato.equals("difettoso")) {
            ordine.setStatoOrdine(new StatoDifettoso());
        }
    }
}

Lo stato StatoEsaurito gestisce le transizioni di stato verso lo stato StatoCancellato come previsto dallo State Diagram.

package patterns.state;

public class StatoEsaurito implements Stato {

    @Override
    public void gestioneStatoOrdine(Ordine ordine, String stato) {
        if (stato.equals("cancellato"))
            ordine.setStatoOrdine(new StatoCancellato());
    }

}

Lo stato StatoCancellato non prevede transizioni di stato

package patterns.state;

public class StatoCancellato implements Stato {

    @Override
    public void gestioneStatoOrdine(Ordine ordine, String stato) {
    }

}

Lo stato StatoPronto gestisce le transizioni di stato verso lo stato StatoInPartenza come previsto dallo State Diagram.

package patterns.state;

public class StatoPronto implements Stato {

    @Override
    public void gestioneStatoOrdine(Ordine ordine, String stato) {
        if (stato.equals("in_partenza"))
            ordine.setStatoOrdine(new StatoInPartenza());
    }

}

Lo stato StatoDifettoso gestisce le transizioni di stato verso lo stato StatoInCorso come previsto dallo State Diagram.

package patterns.state;

public class StatoDifettoso implements Stato {

    @Override
    public void gestioneStatoOrdine(Ordine ordine, String stato) {
        if (stato.equals("in_corso"))
            ordine.setStatoOrdine(new StatoInCorso());
    }

}

Lo stato StatoInPartenza gestisce le transizioni di stato verso lo stato StatoSpedito come previsto dallo State Diagram.

package patterns.state;

public class StatoInPartenza implements Stato {

    @Override
    public void gestioneStatoOrdine(Ordine ordine, String stato) {
        if (stato.equals("spedito"))
            ordine.setStatoOrdine(new StatoSpedito());
    }

}

Lo stato StatoSpedito non prevede transizioni di stato

package patterns.state;

public class StatoSpedito implements Stato {

    @Override
    public void gestioneStatoOrdine(Ordine ordine, String stato) {
    }

}

La classe GestioneOrdine, come nell’esempio precedente, effettua una simulazione di cambi di stato, ma questa volta la transizione di stato è gestita dal singolo stato.

package patterns.state;

public class GestoreOrdine {

    public static void main(String[] args) {
        new GestoreOrdine().esecuzioneOrdine();
    }

    public void esecuzioneOrdine() {
        Ordine ordine = new Ordine();

        Stato statoOrdine = ordine.getStatoOrdine();
        System.out.println("Stato attuale dell'ordine: " + statoOrdine );

        statoOrdine.gestioneStatoOrdine( ordine, "in_corso" );
        statoOrdine = ordine.getStatoOrdine();
        System.out.println("Stato attuale dell'ordine: " + statoOrdine );

        statoOrdine.gestioneStatoOrdine( ordine, "pronto" );
        statoOrdine = ordine.getStatoOrdine();
        System.out.println("Stato attuale dell'ordine: " + statoOrdine );

        statoOrdine.gestioneStatoOrdine( ordine, "in_partenza" );
        statoOrdine = ordine.getStatoOrdine();
        System.out.println("Stato attuale dell'ordine: " + statoOrdine );

        statoOrdine.gestioneStatoOrdine( ordine, "spedito" );
        statoOrdine = ordine.getStatoOrdine();
        System.out.println("Stato attuale dell'ordine: " + statoOrdine );
    }
}

Il risultato dell’elaborazione mostra le stesse transizioni di stato precedenti ma la logica implementativa è diversa:

$JAVA_HOME/bin/java patterns.state.GestioneOrdine
Stato attuale dell'ordine: patterns.state.StatoNuovo@e53108
Stato attuale dell'ordine: patterns.state.StatoInCorso@1befab0
Stato attuale dell'ordine: patterns.state.StatoPronto@c21495
Stato attuale dell'ordine: patterns.state.StatoInPartenza@1034bb5
Stato attuale dell'ordine: patterns.state.StatoSpedito@1cfb549
Categorie:GOF Pattern, java
  1. 21 agosto 2013 alle 11:08 AM

    grazie! bell’esempio!

  2. Giorgio
    3 giugno 2014 alle 7:18 PM

    Ciao Giuseppe,

    innanzitutto grazie per gli esempi esaustivi.

    Avrei una domanda. Sul libro della GoF, il pattern State mi sembra che sia spiegato in maniera leggermente diversa, in quanto il cambiamento effettivo di stato del context viene ottenuto passando il context stesso come parametro nel metodo delle classi che implementano l’interfaccia State e settando attraverso tale reference lo stato, ovvero nel tuo esempio il metodo handle() dell’interfaccia State, dovrebbe così trasfomarsi: handle(Context context) e poi nell’implementazioni dell’interfaccia, si effettua l’overide settando anche lo stato del context:

    handle(Context context){
    context.setState(this);
    //business logic
    }

    Sperando di essermi riuscito a spiegare bene, volevo sapere che ne pensi.

    • 9 giugno 2014 alle 4:56 PM

      Ciao,
      spero di aver capito la tua domanda e nello stesso modo spero di riuscirmi a spiegare bene.
      Dal Diagramma del GoF presente anche su wikipedia: http://it.wikipedia.org/wiki/State_pattern#mediaviewer/File:State_design_pattern.png non è indicato espressamente che il contesto deve essere passato allo stato ma nel libro GOF è precisato:

      A context may pass itself as an argument to the State object handling the request. This lets the State object access the context if necessary

      Questo vuol dire che il contesto “may pass” non “must pass”, ossia “può passare” non “deve passare”, pertanto se non serve “può non essere passato” infatti è anche indicato “if necessary”.
      A parte la traduzione letterale, il concetto è giusto, nel senso che non sempre serve passare il contesto.
      Nel primo esempio che ho fatto, non ho passato il Contesto, mentre l’ho passato nel secondo esempio in quanto mi serviva per recuperare delle informazioni.

      Nel primo esempio la classe di Contesto (Palette) “conosce” lo stato, pertanto “sa” in che stato si trova e quindi conosce il colore.

      public void setColor(Color color) {
              this.color=color;
      }
      

      All’atto dell’esecuzione applica il “principio di delega”, cioè “delega” l’esecuzione del metodo relativo allo Stato corrente (RedColor o WhiteColor):

      public void currentColor() {
          color.showColor();
      }
      
  1. No trackbacks yet.
I commenti sono chiusi.
%d blogger cliccano Mi Piace per questo: