Modèle de conception d'état en Java

1. Vue d'ensemble

Dans ce didacticiel, nous présenterons l'un des modèles de conception comportementaux du GoF - le modèle d'état.

Dans un premier temps, nous donnerons un aperçu de son objectif et expliquerons le problème qu'il tente de résoudre. Ensuite, nous examinerons le diagramme UML de l'État et l'implémentation de l'exemple pratique.

2. Modèle de conception d'état

L'idée principale du modèle d'état est de permettre à l'objet de changer son comportement sans changer sa classe. De plus, en l'implémentant, le code devrait rester plus propre sans beaucoup d'instructions if / else.

Imaginez que nous ayons un colis qui est envoyé à un bureau de poste, le colis lui-même peut être commandé, puis livré à un bureau de poste et finalement reçu par un client. Maintenant, en fonction de l'état réel, nous voulons imprimer son état de livraison.

L'approche la plus simple serait d'ajouter des indicateurs booléens et d'appliquer des instructions if / else simples dans chacune de nos méthodes de la classe. Cela ne compliquera pas beaucoup les choses dans un scénario simple. Cependant, cela pourrait compliquer et polluer notre code lorsque nous aurons plus d'états à traiter, ce qui entraînera encore plus d'instructions if / else.

En outre, toute la logique de chacun des états serait répartie sur toutes les méthodes. Maintenant, c'est là que le modèle d'état peut être envisagé. Grâce au modèle de conception State, nous pouvons encapsuler la logique dans des classes dédiées, appliquer le principe de responsabilité unique et le principe ouvert / fermé, avoir un code plus propre et plus maintenable.

3. Diagramme UML

Dans le diagramme UML, nous voyons que la classe Context a un état associé qui va changer pendant l'exécution du programme.

Notre contexte va déléguer le comportement à l'implémentation de l'état. En d'autres termes, toutes les demandes entrantes seront traitées par la mise en œuvre concrète de l'État.

Nous voyons que la logique est séparée et l'ajout de nouveaux états est simple - cela revient à ajouter une autre implémentation d' état si nécessaire.

4. Mise en œuvre

Concevons notre application. Comme déjà mentionné, le paquet peut être commandé, livré et reçu, nous allons donc avoir trois états et la classe de contexte.

Tout d'abord, définissons notre contexte, cela va être une 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(); } }

Comme nous pouvons le voir, il contient une référence pour gérer l'état, notez les méthodes previousState (), nextState () et printStatus () où nous déléguons le travail à l'objet d'état. Les états seront liés les uns aux autres et chaque état en définira un autre basé sur cette référence transmise aux deux méthodes.

Le client interagira avec la classe Package , mais il n'aura pas à s'occuper de définir les états, tout ce que le client a à faire est de passer à l'état suivant ou précédent.

Ensuite, nous allons avoir le PackageState qui a trois méthodes avec les signatures suivantes:

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

Cette interface sera implémentée par chaque classe d'état concrète.

Le premier état concret sera 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."); } }

Ici, nous indiquons l'état suivant qui se produira après la commande du colis. L'état ordonné est notre état racine et nous le marquons explicitement. Nous pouvons voir dans les deux méthodes comment la transition entre les états est gérée.

Jetons un coup d'œil à la 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."); } }

Encore une fois, nous voyons le lien entre les États. Le paquet change son état de commandé à livré, le message dans printStatus () change également.

Le dernier état est 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()); } }

C'est là que nous atteignons le dernier état, nous ne pouvons que revenir à l'état précédent.

Nous voyons déjà qu'il y a des avantages, car un État connaît l'autre. Nous les rendons étroitement couplés.

5. Test

Voyons comment se comporte l'implémentation. Tout d'abord, vérifions si les transitions de configuration fonctionnent comme prévu:

@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)); }

Ensuite, vérifiez rapidement si notre colis peut reculer avec son état:

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

Après cela, vérifions le changement d'état et voyons comment l'implémentation de la méthode printStatus () modifie son implémentation au moment de l'exécution:

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(); } }

Cela nous donnera le résultat suivant:

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.

Comme nous avons changé l'état de notre contexte, le comportement a changé mais la classe reste la même. En plus de l'API que nous utilisons.

De plus, la transition entre les états s'est produite, notre classe a changé son état et par conséquent son comportement.

6. Inconvénients

L'inconvénient du modèle d'état est le gain lors de la mise en œuvre d'une transition entre les états. Cela rend l'état codé en dur, ce qui est une mauvaise pratique en général.

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

Le modèle de conception d'état est idéal lorsque nous voulons éviter les instructions if / else primitives . Au lieu de cela, nous extrayons la logique pour séparer les classes et laissons notre objet de contexte déléguer le comportement aux méthodes implémentées dans la classe d'état. En outre, nous pouvons tirer parti des transitions entre les états, où un état peut modifier l'état du contexte.

En général, ce modèle de conception est idéal pour les applications relativement simples, mais pour une approche plus avancée, nous pouvons consulter le didacticiel Spring's State Machine.

Comme d'habitude, le code complet est disponible sur le projet GitHub.