State Design Pattern in Java

1. Panoramica

In questo tutorial, introdurremo uno dei modelli di progettazione comportamentali di GoF: il modello di stato.

In un primo momento, daremo una panoramica del suo scopo e spiegheremo il problema che cerca di risolvere. Quindi, daremo uno sguardo al diagramma UML dello Stato e all'implementazione dell'esempio pratico.

2. State Design Pattern

L'idea principale del pattern State è di consentire all'oggetto di cambiare il suo comportamento senza cambiare la sua classe. Inoltre, implementandolo, il codice dovrebbe rimanere più pulito senza molte istruzioni if ​​/ else.

Immagina di avere un pacco che viene inviato a un ufficio postale, il pacco stesso può essere ordinato, quindi consegnato a un ufficio postale e infine ricevuto da un cliente. Ora, a seconda dello stato attuale, vogliamo stamparne lo stato di consegna.

L'approccio più semplice sarebbe aggiungere alcuni flag booleani e applicare semplici istruzioni if ​​/ else all'interno di ciascuno dei nostri metodi nella classe. Ciò non complicherà molto in uno scenario semplice. Tuttavia, potrebbe complicare e inquinare il nostro codice quando avremo più stati da elaborare, il che si tradurrà in ancora più istruzioni if ​​/ else.

Inoltre, tutta la logica per ciascuno degli stati sarebbe diffusa in tutti i metodi. Ora, è qui che si potrebbe considerare di utilizzare il pattern State. Grazie al modello di progettazione dello Stato, possiamo incapsulare la logica in classi dedicate, applicare il Principio di responsabilità unica e il Principio aperto / chiuso, avere un codice più pulito e più manutenibile.

3. Diagramma UML

Nel diagramma UML, vediamo che la classe Context ha uno Stato associato che cambierà durante l'esecuzione del programma.

Il nostro contesto delegherà il comportamento all'implementazione statale. In altre parole, tutte le richieste in arrivo saranno gestite dall'attuazione concreta dello Stato.

Vediamo che la logica è separata e l'aggiunta di nuovi stati è semplice: si tratta di aggiungere un'altra implementazione di stato, se necessario.

4. Implementazione

Progettiamo la nostra applicazione. Come già accennato, il pacchetto può essere ordinato, consegnato e ricevuto, quindi avremo tre stati e la classe di contesto.

Per prima cosa, definiamo il nostro contesto, che sarà una classe Package :

public class Package { private PackageState state = new OrderedState(); // getter, setter public void previousState() { state.prev(this); } public void nextState() { state.next(this); } public void printStatus() { state.printStatus(); } }

Come possiamo vedere, contiene un riferimento per la gestione dello stato, notare i metodi previousState (), nextState () e printStatus () dove deleghiamo il lavoro all'oggetto state. Gli stati saranno collegati tra loro e ogni stato ne imposterà un altro in base a questo riferimento passato a entrambi i metodi.

Il client interagirà con la classe Package , ma non dovrà occuparsi di impostare gli stati, tutto ciò che deve fare è passare allo stato successivo o precedente.

Successivamente, avremo PackageState che ha tre metodi con le seguenti firme:

public interface PackageState { void next(Package pkg); void prev(Package pkg); void printStatus(); }

Questa interfaccia sarà implementata da ogni classe di stato concreto.

Il primo stato concreto sarà OrderedState :

public class OrderedState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new DeliveredState()); } @Override public void prev(Package pkg) { System.out.println("The package is in its root state."); } @Override public void printStatus() { System.out.println("Package ordered, not delivered to the office yet."); } }

Qui, indichiamo lo stato successivo che si verificherà dopo che il pacchetto è stato ordinato. Lo stato ordinato è il nostro stato radice e lo contrassegniamo esplicitamente. Possiamo vedere in entrambi i metodi come viene gestita la transizione tra gli stati.

Diamo un'occhiata alla classe DeliveredState :

public class DeliveredState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new ReceivedState()); } @Override public void prev(Package pkg) { pkg.setState(new OrderedState()); } @Override public void printStatus() { System.out.println("Package delivered to post office, not received yet."); } }

Di nuovo, vediamo il collegamento tra gli stati. Il pacchetto sta cambiando il suo stato da ordinato a consegnato, cambia anche il messaggio in printStatus () .

L'ultimo stato è ReceivedState :

public class ReceivedState implements PackageState { @Override public void next(Package pkg) { System.out.println("This package is already received by a client."); } @Override public void prev(Package pkg) { pkg.setState(new DeliveredState()); } }

Qui è dove raggiungiamo l'ultimo stato, possiamo solo tornare allo stato precedente.

Vediamo già che c'è una certa ricompensa poiché uno stato conosce l'altro. Li stiamo facendo accoppiati strettamente.

5. Test

Vediamo come si comporta l'implementazione. Innanzitutto, verifichiamo se le transizioni di installazione funzionano come previsto:

@Test public void givenNewPackage_whenPackageReceived_thenStateReceived() { Package pkg = new Package(); assertThat(pkg.getState(), instanceOf(OrderedState.class)); pkg.nextState(); assertThat(pkg.getState(), instanceOf(DeliveredState.class)); pkg.nextState(); assertThat(pkg.getState(), instanceOf(ReceivedState.class)); }

Quindi, controlla rapidamente se il nostro pacchetto può tornare indietro con il suo stato:

@Test public void givenDeliveredPackage_whenPrevState_thenStateOrdered() { Package pkg = new Package(); pkg.setState(new DeliveredState()); pkg.previousState(); assertThat(pkg.getState(), instanceOf(OrderedState.class)); }

Dopodiché, verifichiamo di modificare lo stato e vediamo come l'implementazione del metodo printStatus () cambia la sua implementazione in fase di esecuzione:

public class StateDemo { public static void main(String[] args) { Package pkg = new Package(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); } }

Questo ci darà il seguente output:

Package ordered, not delivered to the office yet. Package delivered to post office, not received yet. Package was received by client. This package is already received by a client. Package was received by client.

Dato che stavamo cambiando lo stato del nostro contesto, il comportamento stava cambiando ma la classe rimane la stessa. Oltre all'API che utilizziamo.

Inoltre, la transizione tra gli stati è avvenuta, la nostra classe ha cambiato il suo stato e di conseguenza il suo comportamento.

6. Aspetti negativi

Lo svantaggio del modello di stato è il vantaggio quando si implementa la transizione tra gli stati. Ciò rende lo stato hardcoded, che è una cattiva pratica in generale.

But, depending on our needs and requirements, that might or might not be an issue.

7. State vs. Strategy Pattern

Both design patterns are very similar, but their UML diagram is the same, with the idea behind them slightly different.

First, the strategy pattern defines a family of interchangeable algorithms. Generally, they achieve the same goal, but with a different implementation, for example, sorting or rendering algorithms.

In state pattern, the behavior might change completely, based on actual state.

Next, in strategy, the client has to be aware of the possible strategies to use and change them explicitly. Whereas in state pattern, each state is linked to another and create the flow as in Finite State Machine.

8. Conclusion

Il modello di progettazione dello stato è ottimo quando vogliamo evitare le istruzioni primitive if / else . Invece, estraiamo la logica per separare le classi e lasciamo che il nostro oggetto contesto deleghi il comportamento ai metodi implementati nella classe di stato. Inoltre, possiamo sfruttare le transizioni tra gli stati, dove uno stato può alterare lo stato del contesto.

In generale, questo modello di progettazione è ottimo per applicazioni relativamente semplici, ma per un approccio più avanzato, possiamo dare un'occhiata al tutorial di Spring's State Machine.

Come al solito, il codice completo è disponibile nel progetto GitHub.