Mr TOTO / blog
· Design Pattern · Java · 6 min di lettura

State Pattern — Evitare troppi condizionali

Come rappresentare in modo esplicito gli stati di un oggetto quando il suo comportamento cambia insieme a essi.

Diagramma delle classi che applica il pattern State a un orologio digitale
Ogni modalità dell’orologio diventa un oggetto esplicito con comportamento e transizioni propri.

Spesso costruiamo una classe il cui comportamento dipende da una combinazione di valori interni. La prima implementazione si affida normalmente a un numero crescente di if o a un grande switch. Man mano che aggiungiamo modalità e azioni, la logica diventa sempre più difficile da comprendere e mantenere.

Il pattern State della Gang of Four affronta questo problema permettendo a un oggetto di cambiare comportamento quando cambia il suo stato interno.

Un orologio con due pulsanti

Immaginiamo un orologio digitale con due pulsanti: MODE e CHANGE. Premendo MODE, l’orologio passa attraverso tre stati:

  • visualizzazione normale;
  • modifica dell’ora;
  • modifica dei minuti.

Il significato di CHANGE dipende dallo stato corrente:

  • nella visualizzazione normale accende la luce;
  • durante la modifica dell’ora incrementa l’ora;
  • durante la modifica dei minuti incrementa i minuti.

Un’implementazione condizionale potrebbe iniziare così:

void pressChange() {
    if (state == NORMAL_DISPLAY) {
        activateLight();
    } else if (state == UPDATING_HOURS) {
        hours++;
    } else if (state == UPDATING_MINUTES) {
        minutes++;
    }
}

Il codice funziona, ma ogni nuovo stato ci obbliga a rivedere più operazioni. Le regole appartenenti a uno stato sono sparse tra diversi metodi, rendendo difficile vedere il comportamento completo dell’orologio.

Struttura del pattern

La struttura generale del design pattern State

Il pattern separa tre ruoli:

  • Context è l’oggetto utilizzato dal client. Conserva un riferimento allo stato corrente e delega a esso le azioni dipendenti dallo stato.
  • State è un’interfaccia o una classe astratta che definisce le azioni condivise da ogni possibile stato.
  • Concrete State rappresenta una modalità del context. Implementa quelle azioni e può spostare il context verso un altro stato.

Nel nostro orologio, il context delega entrambi i pulsanti:

public class Clock {
    private ClockState state;
    int hour;
    int minute;

    public Clock() {
        state = new NormalDisplayState(this);
    }

    void setState(ClockState state) {
        this.state = state;
    }

    public void pressMode() {
        state.pressMode();
    }

    public void pressChange() {
        state.pressChange();
    }
}

Il contratto comune è piccolo:

public abstract class ClockState {
    protected final Clock clock;

    protected ClockState(Clock clock) {
        this.clock = clock;
    }

    public abstract void pressMode();
    public abstract void pressChange();
}

Ogni stato concreto mantiene raccolte le proprie regole:

public class NormalDisplayState extends ClockState {
    public NormalDisplayState(Clock clock) {
        super(clock);
    }

    public void pressMode() {
        clock.setState(new UpdatingHourState(clock));
    }

    public void pressChange() {
        System.out.println("Light on");
    }
}

public class UpdatingHourState extends ClockState {
    public UpdatingHourState(Clock clock) {
        super(clock);
    }

    public void pressMode() {
        clock.setState(new UpdatingMinuteState(clock));
    }

    public void pressChange() {
        clock.hour = (clock.hour + 1) % 24;
    }
}

public class UpdatingMinuteState extends ClockState {
    public UpdatingMinuteState(Clock clock) {
        super(clock);
    }

    public void pressMode() {
        clock.setState(new NormalDisplayState(clock));
    }

    public void pressChange() {
        clock.minute = (clock.minute + 1) % 60;
    }
}

I vantaggi

Ogni stato descrive ora un insieme coerente di comportamenti e transizioni. Aggiungere uno stato richiede ancora una progettazione consapevole, ma non richiede più un nuovo ramo in ogni metodo del context che dipende dallo stato.

Il pattern è particolarmente utile quando gli stati rappresentano concetti importanti e ciascuno porta con sé comportamenti diversi. Per una piccola condizione fissa, un semplice if o switch può restare più chiaro. L’obiettivo non è eliminare ogni condizionale, ma rendere visibile la macchina a stati quando è già parte del problema.

Riferimenti