Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUMS JAVA FAQs TUTORIELS JAVASEARCH SOURCES LIVRES OUTILS, EDI & API ECLIPSE NETBEANS BLOG DISCUSSIONS TV
Trucs et astuces
Validité des paramètres
Documenter votre code
Assertions
Tests unitaires
Design patterns - GOF
Adaptateur
Composite
Décorateur
etat
Façade
Kit
Monteur
Pont
Proxy
Singleton
Design patterns - Avalon
IOC - Inversion Of Control
SOC - Seperation Of Concerns
COP - Component Orientated Programming
SOP - Service Orientated Programming
Autres articles
Cahier
XML Sax en java
Fractal
AspectJ

Voir aussi
Patterns du GRASP
héritage avec des EJB Entiy CMP

Ressouces java
Informations
Cours
Livres
FAQ
Outils
EDI
Ressources uml
Cours
Livres
Forums d'entraide
Géneral Java
J2EE
JBuilder
Outils EDI
Méthodes/UML/Mérise


L'état - Uml design pattern
20 Septembre 2003
Version 1.0
Par Sébastien MERIC
Remerciements : Stefan Bertholon

Synopsis

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é.

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. Etudions 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 appaîtront mal dans ces diagrammes. Pourtant le diagramme suivant décrit à merveille celle-ci.

Diagramme d'état

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.

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 :

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
  • Ecrire 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 á 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 á 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

/** 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 á 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

/** 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 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

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 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;
 }
Copyright (c) 2003 Sébastien MERIC. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with the Front-Cover Texts being L'état - Uml design pattern, and Back-Cover Texts being Ce document à été écrit pour la communauté de développeurs francophones "www.developpez.com". A copy of the license is included in the section entitled "GNU Free Documentation License".
/java/uml/etat  

Responsables bénévoles de la rubrique Java : Christophe Jollivet et Eric Siber - Contacter par EMail :
Vos questions techniques : forum d'entraide Java - Publiez vos articles, tutoriels et cours
et rejoignez-nous dans l'équipe de rédaction du club d'entraide des développeurs francophones
Nous contacter - Copyright © 2000-2008 www.developpez.com - Legal informations.