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

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 :
- Lister les triggers et les présenter dans l'interface
- Ecrire les classes EtatXXX qui implémentent cette interface
- Lier le contexte à l'interface et instancier l'état de départ
- Implémenter le changement d'état du contexte
- 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;
}
|