L'état - Uml design pattern

Sébastien MERIC

Permet de modifier le comportement d'un objet lorsque son état est modifié. Tout est mis en place pour donner l'impression que l'objet lui-même a été modifié.

- Titre : L'état - Uml design pattern

- Auteur : Sébastien MERIC

- Parution : 20 septembre 2003

- Mise à jour : 19 juin 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Sébastien MERIC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Exploration

- II. Structure

- III. Implémentation

I - Exploration

Tout informaticien a rencontré un jour la notion de machine de turing, les machines à états qui sont les fondations de toute l'informatique. Ceux d'entre vous qui sont plus précisément développeurs en informatique embarquée connaissent très bien la notion de machine à états étendue. Mal, voire pas utilisée en informatique de gestion, cette notion est pourtant d'une grande puissance. Elle permet de bien découpler le traitement de l'enchaînement de celui-ci. Étudions un diagramme d'états pour bien nous en rendre compte.

Imaginons pour la circonstance la mini spécification suivante. Nous gérons des Banques et leurs agences associées. Une banque sera décrite par son nom et l'ensemble de ses agences. Une banque doit avoir un nom. On peut ajouter des agences ou en supprimer dans une banque. Pour supprimer une banque, nous devons nous assurer qu'il n'existe pas d'agence associée.

C'est volontairement simple, mais permet de bien mettre la notion et la puissance des machines à états en évidence même en gestion. Nous devons décrire une dynamique, si possible sous la forme d'un diagramme, et la première chose en général qui nous vient à l'esprit pour la décrire, c'est le diagramme de séquences. Or, si vous essayez de décrire la petite spécification précédente sous la forme de diagrammes de séquences, vous allez être obligé d'écrire un minimum de deux diagrammes et les contraintes apparaîtront mal dans ces diagrammes. Pourtant le diagramme suivant décrit à merveille celle-ci.

Comme nous pouvons l'observer sur ce diagramme, tout est décrit de manière très lisible : si l'initialisation n'a pas eu lieu, il est impossible d'ajouter des agences. S'il existe des agences, nous ne pouvons pas supprimer la banque. En effet, une banque admet trois états : non initialisée (pas de nom correct), initialisés, mais sans agence associée ou initialisée et associée à des agences. Ensuite, nous observons les trajets possibles d'un état à l'autre à l'aide des transitions. Enfin, les triggers et les gardes nous permettent de savoir dans quelles conditions nous passons d'un état à l'autre.

Il ne me reste plus qu'à vous présenter la structure objet d'une machine à états avant de vous en proposer une implémentation java complète.

II - Structure

Nous désirons donc représenter de manière élégante dans un diagramme de classes UML d'une part, la classe capable de changer d'états et d'autre part, l'ensemble des états que cette classe peut prendre. L'implémentation classique de cette notion revient en général à instancier un certain nombre de constantes (entières) ETAT_INITIALISE, ETAT_A_DES_AGENCES, etc. puis de définir un attribut état qui représentera cet état et que nous testerons systématiquement avant de traiter les actions associées aux transitions. Cette méthode se solde rapidement, et au plus tard à la première maintenance, par une lecture très difficile des flux.

Afin d'éviter cette implémentation qui rendra à terme très coûteuse la maintenance de notre objet, nous pouvons décider de découpler la réaction de l'objet à ses impulsions de l'objet lui-même. Pour ce faire, nous définissons une interface qui réunit l'ensemble des comportements possibles de notre classe, puis nous implémentons ces divers comportements en fonction de l'état dans lequel nous nous trouvons. Enfin, la classe principale est liée à un état et ceci permet de modifier dynamiquement l'état et donc le comportement de la classe en fonction de l'état dans lequel elle se trouve.

Voici le diagramme de classe UML de la structure de la machine à états :

III - Implémentation

Reprenons l'exemple et essayons de détailler à partir du diagramme d'états UML ce qu'il faut présenter dans notre diagramme de classes. Voyons par étapes, le travail à effectuer :

1. Lister les triggers et les présenter dans l'interface ;

2. Écrire les classes EtatXXX qui implémentent cette interface ;

3. Lier le contexte à l'interface et instancier l'état de départ ;

4. Implémenter le changement d'état du contexte ;

5. Implémenter les états.

Soit dans notre cas :

- liste des triggers :

- validate,

- addAgence,

- delAgence ;

- écrire les états :

- NonValide,

- Valide,

- ADesAgences.

Voici le diagramme de classes UML qu'on en déduit :

Et l'implémentation Java correspondante :

com.developpez.etat.Agence

/*
* Created on 21 sept. 03
*/
package com.developpez.etat;

/**
* @author smeric
*/
public class Agence {

public Agence() {
super();
}

}

com.developpez.etat.Banque

/** Java class "Banque.java" generated from Poseidon for UML.
*  Poseidon for UML is developed by <A HREF="http://www.gentleware.com">Gentleware</A>.
*  Generated with <A HREF="http://jakarta.apache.org/velocity/">velocity</A> template engine.
*/
package com.developpez.etat;

import java.util.*;

/**
* La classe représentant  la banque. Pour l'exemple cette classe est très simplifiée.
*/
public class Banque {

public Banque() {
etat = new EtatNonInitialise(this);
agences = new ArrayList();
}

/**
* valide la banque permettant ensuite l'insertion de nouvelles agences par exemple.
*/
public void validate() {
etat.validate();
}

/**
* Ajoute une agence à la banque
* @param agence
*/
public void addAgence(Agence agence) {
etat.addAgence(agence);
}

/**
* Enlève l'agence de la banque et la supprime
* @param agence l'agence a supprimer
*/
public void delAgence(Agence agence) {
etat.delAgence(agence);
}

/**
* efface la banque
*/
public void delete() {
etat.delete();
}

public Collection getAgences() {
return agences;
}

public String getNom() {
return nom;
}

public void setAgences(Collection collection) {
agences = collection;
}

public void setNom(String string) {
nom = string;
}

void setEtat(EtatBanque etat){
this.etat = etat;
}

private Collection agences;
private EtatBanque etat;
private String nom;

}

com.developpez.etat.EtatBanque

/** Java interface "EtatBanque.java" generated from Poseidon for UML.
*  Poseidon for UML is developed by <A HREF="http://www.gentleware.com">Gentleware</A>.
*  Generated with <A HREF="http://jakarta.apache.org/velocity/">velocity</A> template engine.
*/
package com.developpez.etat;

/**
* Interface représentant les états que peut prendre la banque
*/
public interface EtatBanque {

/**
* réponse à l'impulsion de l'Agence. Suppression d'une agence de la banque.
* @param agence l'agence a supprimer
*/
public void delAgence(Agence agence);
/**
* Efface la banque.
*/
public void delete();
/**
* Ajoute une agence à la banque
* @param agence l'agence a ajouter
*/
public void addAgence(Agence agence);
/**
* valide les informations de la banque.
*/
public  void validate();

}

com.developpez.etat.EtatBanqueNonInitialisee

/** Java class "EtatBanqueNonInitialisee.java" generated from Poseidon for UML.
*  Poseidon for UML is developed by <A HREF="http://www.gentleware.com">Gentleware</A>.
*  Generated with <A HREF="http://jakarta.apache.org/velocity/">velocity</A> template engine.
*/
package com.developpez.etat;

/**
* Cette classe représente l'état de départ de la banque avant initialisation, c'est-à-dire pour
* l'exemple simple que nous avons pris : le nom est null. Nous ne réagirons dans cet état qu'à
* l'impulsion validate() et sous la condition que le nom de la banque soit initialisé.
* @author smeric
*/
public class EtatBanqueNonInitialisee implements EtatBanque {

public EtatBanqueNonInitialisee(Banque banque) {
if (null == banque) { // hé hé on représente l'état d'une banque s'il vous plaît
throw new IllegalArgumentException("L'etat d'une banque est
necessairement associe a la banque qu'il represente");
}
this.banque = banque;
}

/**
* Réponse à l'impulsion delAgence, ne peut pas être prise en compte dans cet etat
* @param agence l'agence à supprimer.
* @throws IllegalStateException systématiquement, car l'état ne répond pas a cette
* impulsion.
* @see com.developpez.etat.EtatBanque#delAgence(com.developpez.etat.Agence)
*/

public void delAgence(Agence agence) {
throw new IllegalStateException("L'agence n'est pas initialisee,
impossible d'ajouter ou supprimer des agences");
}

/**
* Réponse à l'impulsion delete qui n'est pas possible depuis cet état non initialisé.
* @throws IllegalStateException systématiquement, car l'état ne répond pas à cette
* impulsion.
* @see com.developpez.etat.EtatBanque#delete()
*/
public void delete() {
throw new IllegalStateException("L'agence n'est pas initialisee, impossible
d'ajouter ou supprimer des agences");
}

/**
* Toujours pas de réponse dans ce cas
* @throws IllegalStateException systématiquement, car l'état ne répond pas a cette
* impulsion.
* @see com.developpez.etat.EtatBanque#addAgence(com.developpez.etat.Agence)
*/
public void addAgence(Agence agence) {
throw new IllegalStateException("L'agence n'est pas initialisee, impossible d'ajouter
ou supprimer des agences");
}

/**
* Ça y est il y a une transition au départ de cet état qui répond à l'impulsion validate
* nous allons pouvoir remplir de manière consistante cette méthode.<br/>
* La validation permet simplement de passer à l'état initialisé si le nom de la banque
* n'est pas vide.
* @see com.developpez.etat.EtatBanque#validate()
*/
public void validate() {

// correspond à la garde décrite sur la transition
if (null == banque.getNom()) {
throw new IllegalStateException("Le nom de banque doit etre non null");
}

//tout est bien, nous sommes dans l'état non initialisé, nous avons reçu l'impulsion
//validate() et la garde est respectée, il ne reste plus qu'à changer d'état

banque.setEtat(new EtatBanqueInitialisee(banque));
}

private final Banque banque;
}

com.developpez.etat.EtatBanqueInitialisee

/** Java class "EtatBanqueInitialisee.java" generated from Poseidon for UML.
*  Poseidon for UML is developed by <A HREF="http://www.gentleware.com">Gentleware</A>.
*  Generated with <A HREF="http://jakarta.apache.org/velocity/">velocity</A> template engine.
*/
package com.developpez.etat;

/**
* Cette classe représente l'état initialise, mais sans agence d'une banque.
*/
public class EtatBanqueInitialisee implements EtatBanque {

public EtatBanqueInitialisee(Banque banque) {
if (null == banque) { // hé hé on represente l'état d'une banque s'il vous plaît
throw new IllegalArgumentException("L'etat d'une banque est necessairement associe a la banque qu'il represente");
}
this.banque = banque;
}

/**
* Pas d'agence enregistrée pour le moment donc impossible d'en supprimer
* @see com.developpez.etat.EtatBanque#delAgence(com.developpez.etat.Agence)
*/
public void delAgence(Agence agence) {
throw new IllegalStateException("La banque ne contient pas d'agence, impossible d'en supprimer");
}

/**
* Nous n'avons pas d'agence, il est donc possible de supprimer cette banque
* @see com.developpez.etat.EtatBanque#delete()
*/
public void delete() {
// pas de garde on passe donc sans problème

//code de suppression de la banque
}

/**
* Ajoutons donc cette agence
* @see com.developpez.etat.EtatBanque#addAgence(com.developpez.etat.Agence)
*/
public void addAgence(Agence agence) {
// pas de garde sur cette transition donc rien a verifier ?

if (null == agence) { // evitons quand meme d'ajouter "rien"
throw new IllegalArgumentException("Impossible d'ajouter une agence nulle");
}

banque.getAgences().add(agence);

// et la transition
banque.setEtat(new EtatBanqueADesAgences(banque));
}

/**
* On à déjà validé cette banque c'est assez inutile de recommencer, mais ce n'est pas grave
* donc je ne lève pas d'exception
* @see com.developpez.etat.EtatBanque#validate()
*/
public void validate() {
}

private final Banque banque;
}

com.developpez.etat.EtatBanqueADesAgences

package com.developpez.etat;

/**
* Représente l'état d'une banque valide et ayant des agences associées.
*/
public class EtatBanqueADesAgences implements EtatBanque {

public EtatBanqueADesAgences(Banque banque) {
if (null == banque) { // hé hé on représente l'état d'une banque s'il vous plaît
throw new IllegalArgumentException("L'etat d'une banque est
nécessairement associé à la banque qu'il represente");
}
this.banque = banque;
}
/**
* Suppression d'une agence et éventuellement retour dans l'état initialisé.
* @see com.developpez.etat.EtatBanque#delAgence(com.developpez.etat.Agence)
*/
public void delAgence(Agence agence) {
// pas de garde précise, mais on va quand même vérifier que l'agence n'est pas
// nulle et qu'elle appartient bien à cette banque.

if (null == agence) { // pas d'agence précisée.
throw new IllegalArgumentException("Impossible de supprimer une agence vide");
}

if (! banque.getAgences().contains(agence)) { // c'est pas mon agence
throw new IllegalArgumentException("Tentative de suppression d'une agence
qui n'est pas associee a cette banque");
}

banque.getAgences().remove(agence);

// et la transition
if (banque.getAgences().isEmpty()) { // retour à l'état initialisé
banque.setEtat(new EtatBanqueInitialisee(banque));
}
}

/**
* effacement de la banque impossible depuis cet état
* @see com.developpez.etat.EtatBanque#delete()
*/
public void delete() {
throw new IllegalStateException("Impossible de supprimer une banque
qui contient encore des agences");
}

/**
* ajout d'une agence
* @see com.developpez.etat.EtatBanque#addAgence(com.developpez.etat.Agence)
*/
public void addAgence(Agence agence) {
if (null == agence) { // je n'ajoute que des agences qui soient effectivement des agences
throw new IllegalArgumentException("Impossible d'ajouter une instance
vide d'agence");
}

banque.getAgences().add(agence);

// pas de transition vers un autre état.
}

/**
* On à déjà valider cette banque c'est assez inutile de recommencer, mais ce n'est pas grave
* donc je ne lève pas d'exception
* @see com.developpez.etat.EtatBanque#validate()
*/
public void validate() {
}

private final Banque banque;
}