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 êtes plus précisément développeurs en informatique embarquée connaissez 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 vienne à 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.

Image non disponible

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ée 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éunie 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 :

Image non disponible

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. Ecrire 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 :

Image non disponible

Et l'implémentation java correspondante :

com.developpez.etat.Agence

 
Sélectionnez
/*
 * Created on 21 sept. 03
 */
package com.developpez.etat;
 
/**
 * @author smeric
 */
public class Agence {
 
        public Agence() {
                super();
        }
 
}
 

com.developpez.etat.Banque

 
Sélectionnez
/** 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 á 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

 
Sélectionnez
/** 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 á supprimer
 */
    public void delAgence(Agence agence);
/**
 * Efface la banque. 
 */
    public void delete();
/**
 * Ajoute une agence à la banque
 * @param agence l'agence á ajouter
 */
    public void addAgence(Agence agence);
/**
 * valide les informations de la banque.
 */
    public  void validate();
 
}
 

com.developpez.etat.EtatBanqueNonInitialisee

 
Sélectionnez
/** 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) { //   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 á cette         
         * impultion.
         * @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 á 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");
        }
 
        /**
         * Ca 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

 
Sélectionnez
/** 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) { //   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 grâve
         * 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

 
Sélectionnez
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) { //   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 un 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 grâve
         * donc je ne lève pas d'exception
         * @see com.developpez.etat.EtatBanque#validate()
         */
        public void validate() {
        }
 
        private final Banque banque;
 }