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 :
- Lister les triggers et les présenter dans l'interface ;
- Écrire 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 ;
-
écrire les états :
- NonValide,
- Valide,
- ADesAgences.
Voici le diagramme de classes UML qu'on en déduit :
Et l'implémentation Java correspondante :
/*
* Created on 21 sept. 03
*/
package
com.developpez.etat;
/**
*
@author
smeric
*/
public
class
Agence {
public
Agence
(
) {
super
(
);
}
}
/** 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;
}