STATE

Come sappiamo, le macchine a stati finiti sono uno dei fondamenti teorici dell’informatica: si tratta di oggetti matematici che modellano sistemi in grado di evolvere, ovvero il cui comportamento varia in base allo stato in cui si trovano.

Volendo rappresentare un oggetto di questo tipo la prima idea potrebbe essere quella di realizzare il cambio di comportamento con una serie di if e switch, un approccio che come abbiamo già visto numerose volte diventa presto difficilmente sostenibile.
In alternativa ad esso si introduce invece lo State pattern che mantenendo l’astrazione delle macchine a stati finiti permette di modellare facilmente il cambiamento di comportamento di un oggetto al modificarsi dello stato. Si noti che rimanendo legato al concetto di automa a stati finiti uno dei punti di forza di questo pattern è la semplicità di apportare delle modifiche al codice quando le specifiche di ciò che è stato modellato tramite una macchina a stati finiti cambiano.

Un esempio di utilizzo di questo pattern potrebbe essere un software di editing di foto, in cui l’utente ha a disposizione una toolbar con diversi strumenti che gli permettono di compiere operazioni diverse sullo stesso piano di lavoro (comportamenti diversi dell’azione “tasto sinistro sullo schermo” in base al tool selezionato).

In un automa a stati finiti le componenti fondamentali sono tre:

  • gli stati, tra cui si distingue lo stato corrente;
  • le azioni che si possono intraprendere in qualunque stato;
  • le transizioni da uno stato all’altro come effetto ulteriore di un’azione (es. vim che con ‘i’ entra in modalità inserimento se era in modalità controllo).

Come si vede dallo schema UML, il pattern State cerca di modellare ciascuna di queste componenti: un’interfaccia State raggruppa la definizione di tutte le azioni, rappresentate da metodi, mentre una classe concreta per ogni stato definisce che effetto hanno tali azioni quando ci si trova al suo interno con l’implementazione dei suddetti metodi.
Infine, una classe Context contiene un riferimento ad uno stato che rappresenta lo stato corrente e delega ad esso la risposta alle azioni (che possono essere viste come degli “eventi”); essa espone inoltre un metodo setState(State) che permette di modificare lo stato corrente.

public class Context {
    private State state;

    public void setState(@NotNull State s) {
        state = s;
    }

    public void sampleOperation() {
        state.sampleOperation(this)
    }
}

Rimane dunque solo da definire come si realizzano le transizioni di stato: chi ha la responsabilità di cambiare lo stato corrente? Esistono due diversi approcci, ciascuno dei quali presenta delle criticità:

  • gli State realizzano le transizioni: volendo rimanere aderenti al modello degli automi a stati finiti, possiamo permettere che gli stati concreti chiamino il metodo setState del Context all’interno della loro implementazione dei metodi se come effetto di un’azione lo stato corrente cambia. Tuttavia, poiché setState chiede in input lo stato a cui transizionare questo approccio richiede che gli stati si conoscano tra di loro: si introduce così una dipendenza tra stati non chiaramente visibile nello schema UML e si ha uno sparpagliamento della conoscenza sulle transizioni che rende questo metodo un po’ “sporco”.

  • il Context realizza le transizioni: con questa seconda strategia è compito del contesto eseguire le transizioni di stato, evitando così che gli stati si debbano conoscere; l’unico depositario della conoscenza sulle transizioni è la classe Context. Ciascuna azione viene dunque intrapresa in due step: il Context richiama il corrispondente metodo dello stato corrente e successivamente ne intercetta il risultato; può dunque decidere tramite esso se cambiare stato e eventualmente a quale stato transizionare.
    Si tratta tuttavia di un ritorno al table-driven design fatto di if e switch da cui ci eravamo voluti allontanare: come in quel caso, l’approccio risulta fattibile soltanto finché ci sono poche possibili transizioni. Inoltre, se una transizione non dipende dal risultato di un’azione ma da come questa è stata eseguita questo approccio è totalmente impossibile in quanto tale tipo di conoscenza non è presente nella classe Context.

Per via delle difficoltà poste dal secondo approccio si sceglie spesso di effettuare le transizioni all’interno degli stati: questo permette di rendere esplicito e atomico il passaggio di stato.
A tal proposito, è interessante notare come le istanze degli stati concreti non posseggano alcuna informazione di stato in quanto il Context a cui si riferiscono viene passato loro al momento della chiamata dei rispettivi metodi: al di là della loro identità essi sono completamente stateless. Si tratta di un approccio molto utile in caso si debbano modellare più macchine a stati finiti dello stesso tipo, in quanto l’assenza di stato rende le stesse istanze degli stati concreti condivisibili tra diversi Context, in una sorta di pattern Singleton.

Volendo trovare ulteriori analogie con altri pattern, il pattern State ricorda nello schema il pattern Strategy: la differenza sta però nel fatto che i diversi stati concreti sono a conoscenza l’uno dell’altro, mentre le strategie erano tra di loro completamente indipendenti, inoltre solitamente nello Strategy c’è un metodo che specifica come realizzare un compito indipendentemente dallo stato dell’oggetto.