Améliorez vos logiciels avec le pattern Etat

Pierre Caboche

Vous souhaitez introduire de la « qualité logicielle » au sein de vos applications ? Sans augmenter les coûts ? En facilitant la maintenance ? En rendant votre programme évolutif ? Vous faites de la Programmation Orientée Objets ? Alors cet article devrait vous intéresser…

- Titre : Améliorez vos logiciels avec le pattern Etat

- Auteur : Pierre Caboche

- Parution : 13 juin 2006

- Mise à jour : 16 juin 2020

- Licence : Copyright ® 2006 Pierre Caboche. 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

- Introduction

- I. Les enjeux

- II. Le Pattern « Etat »

- II-A. Principe

- II-B. Différents types d'états

- II-C. Implémentation

- II-C-1. Hiérarchie de classes

- II-C-2. États avec données intrinsèques

- II-C-3. États sans données intrinsèques

- II-C-4. Changements d'état

- II-D. Exemples

- II-D-1. Automate pour protocole réseau

- II-D-2. Un parseur SAX clair et efficace

- III. Développer avec le Pattern Etat

- III-A. Conception

- III-B. Réalisation

- III-C. Test

- III-D. Évolutivité

- IV. Bonnes pratiques de conception

- IV-A. Proscrire les copier/coller

- IV-B. Organisation en modules

- En C++

- En Java

- V. Avantages et inconvénients

- V-A. Avantages

- V-B. Inconvénients

- VI. Applications

- VI-A. Définition et implémentation de protocoles

- VI-B. Interface utilisateur

- VI-C. Traitement de fichiers

- VI-D. Intelligence Artificielle

- VII. Conclusion

- VIII. Remerciements

- IX. Téléchargement

Introduction

Cet article parle de la manière d'utiliser au mieux le Pattern Etat, issu du livre Design Patterns: Elements of Reusable Object-Oriented Software par Erich Gamma et consors. Le Pattern Etat, lorsque bien utilisé, permet de concevoir des applications de qualité (facile à comprendre, à déboguer, à maintenir, à faire évoluer) sans augmentation notable des charges de travail (temps de maintenance diminués, plus grande évolutivité).

Dans cet article, les exemples sont donnés en C++. J'ai choisi le C++, car vous pouvez très facilement adapter le code à d'autres langages comme Java. L'inverse n'est pas forcément évident (un oubli dans l'implémentation C++ peut conduire à des fuites mémoire, par exemple).

Les diagrammes ont été réalisés avec Dia, logiciel gratuit et multiplateforme pour faire des schémas de toutes sortes (UML et autres).

I - Les enjeux

Plutôt qu'un long discours, j'ai préféré vous faire une démonstration simple de ce que peut vous apporter le Pattern Etat.

Voici ce à quoi peut très vite ressembler le code d'une application (par exemple une application réseau) :

int MaClasse::run() {
...

while (executer) {

if ((etat == ETAT1) || (etat == ETAT2)) {

// du code

} elseif (etat == ETAT3) {

// du code

} else {

// encore du code

}

switch (etat) {

case ETAT2:
case ETAT3:
case ETAT5:

// encore du code

if (etat == ETAT5) {
// du code, uniquement si on est dans l'état 5
}

break;

case 1:
// du code, pour l'état 1

// pas de break, histoire d'ajouter à la confusion ambiante

case 4:
// encore du code
}

...
}

}

C'est illisible, difficile à tracer, sujet à de nombreux bugs, pas du tout évolutif, etc. Pour des applications complexes, cela devient très vite ingérable.

Maintenant, regardons ce code :

int MaClasse::run() {

etat = EtatInitial::getInstance() ;

while( etat->executer(this) ) ;

return etat->codeRetour() ;
}

C'est beaucoup plus propre :

- l'implémentation est encapsulée dans différentes classes Etat
(1 comportement = 1 classe Etat) ;

- l'exécution est facile à tracer
(mise en évidence des états du système, ainsi que des transitions entre états).

Nous verrons ceci à partir de la section suivante.

II - Le Pattern « Etat »

II-A - Principe

Le pattern Etat permet de déléguer le comportement d'un objet dans un autre objet. Cela permet de changer le comportement de l'objet en cours d'exécution et de simuler un changement de classe.

Voici le diagramme de classes correspondant au pattern Etat :

Le pattern Etat

Ici, le comportement des méthodes de la classe Application est défini dans la méthode correspondante de la classe Etat :

int UneClasse::methode1() {
return _etat->methode1( this ) ;
}

Voici le code détaillé :

Fichier .h

// Declaration des classes
class UneClasse ;
class Etat ;

/*
NOTE IMPORTANTE :

La déclaration des classes UneClasse et Etat est nécessaire en C++, car
chacune de ces classes a besoin de connaître l'existence de l'autre
(la classe UneClasse possède un attribut état, la méthode 'execute' de
la classe Etat a comme argument 'sender', de type UneClasse)
*/

class UneClasse {

protected :
Etat * _etat ;

public :
// Constructeur
UneClasse() ;

// Destructeur
virtual ~UneClasse() ;

// Modifie l'état
void setState(Etat *) ;

// Les méthodes
int methode1 () ;

int methode2 (int param1, int param2) ;

} ;

class Etat {

public :
// Destructeur
virtual ~Etat() ;

// Les méthodes (virtuelles)

virtual int methode1(UneClasse * sender) = 0 ;
virtual int methode2(UneClasse * sender, int param1, int param2) = 0 ;
} ;

Fichier .cpp

/************************************
*
* classe UneClasse
*
***********************************/

// Constructeur
UneClasse::UneClasse() {

}

// Destructeur
UneClasse::~UneClasse() {

}

void UneClasse::setState(Etat * nouvelEtat) {
_etat = nouvelEtat ;
}

int UneClasse::methode1() {
return _etat->methode1( this ) ;
}

int UneClasse::methode2( int param1, int param2 ) {
return _etat->methode2( this, param1, param2 ) ;
}

/************************************
*
* classe Etat (abstraite)
*
***********************************/

Etat::~Etat() {
// Destructeur de la super classe, appelé par toutes les sous-classes
}

II-B - Différents types d'états

Il existe deux types d'états :

- les états avec données intrinsèques ;

- les états sans données intrinsèques.

Les données intrinsèques sont des attributs qui sont propres à un objet Etat (et non à une classe). On ne peut donc pas les déclarer comme static et l'état ne peut pas être déclaré comme Singleton (il ne peut exister d'instance unique de cet état).

Ces 2 types d'états diffèrent donc de par leur implémentation. Les états avec données intrinsèques font obligatoirement appel à des constructeurs et un destructeur publiques. Les états sans données intrinsèques sont le plus souvent implémentés sous la forme d'un Singleton.

Tous les états (avec ou sans données intrinsèques) manipulent les données de la classe Principale appelant leurs méthodes. La plupart des données manipulées se trouvent donc au niveau la classe Principale (appelante). Les états possédant des données intrinsèques sont généralement des états que l'on doit garder en mémoire pour diverses raisons. Nous verrons un peu plus loin un exemple de chaque (états avec et sans données intrinsèques).

II-C - Implémentation

II-C-1 - Hiérarchie de classes

Qu'ils soient avec ou sans données intrinsèques, les états s'organisent toujours selon une hiérarchie. Généralement, cette hiérarchie ne contient que deux niveaux: celui de la super-classe (abstraite, généralement abstraite pure) et celui des classes concrètes (contenant les diverses implémentations).

Hiérarchie de classes

Voici le prototype de telles classes :

fichier .h

class Etat {

public :
// Destructeur
virtual ~Etat() ;

// Les méthodes (virtuelles)

virtual int methode1(Principale * sender) = 0 ;
virtual int methode2(Principale * sender) = 0 ;
} ;

namespace Etat_Principale {

class EtatInitial : public Etat {

public :
// Constructeur
EtatInitial() ;

// Destructeur
virtual ~EtatInitial() ;

// Les méthodes

virtual int methode1(Principale * sender) ;
virtual int methode2(Principale * sender) ;
} ;

}

II-C-2 - États avec données intrinsèques

Les états avec données intrinsèques sont implémentés comme la plupart des classes en C++: avec un (ou des) constructeur(s) et un destructeur.

Il incombe donc au programmeur de détruire les états dont il ne se sert plus (pour éviter les fuites mémoire).

fichier .h

namespace Etat_Principale {

class Etat_AvecDonnees : public Etat {

protected :
int _donneeIntrinseque1 ;
int _donneeIntrinseque2 ;

public :
// Constructeurs
Etat_AvecDonnees(int uneDonnee) ;
Etat_AvecDonnees(int uneDonnee, int uneAutreDonnee) ;

// Destructeur
virtual ~Etat_AvecDonnees() ;
} ;

}

II-C-3 - États sans données intrinsèques

Les états sans données intrinsèques sont le plus souvent implémentés comme des Singletons, c'est-à-dire que le constructeur n'est pas public (on le déclare comme protected, afin de pouvoir créer des sous-classes) et qu'on y accède au moyen de la méthode statique getInstance().

Cela permet de garder le contrôle sur le nombre d'instances d'états dans le programme et de disposer d'une interface simple pour accéder aux différents états (au moyen de la méthode statique getInstance).

En implémentant de tels états sous la forme de Singleton, on dispose d'une interface simple pour invoquer l'état en question (au moyen de la méthode statique getInstance).

fichier .h

namespace Etat_Principale {

class Etat_SansDonnees : public Etat {

protected :
// Constructeur
Etat_SansDonnees() ;

Etat_SansDonnees * _instance = 0 ;

public :
// Destructeur
virtual ~Etat_SansDonnees() ;

static Etat_SansDonnees * getInstance() ;
} ;

}

fichier .cpp

using namespace Etat_Principale ;

Etat_SansDonnees * Etat_SansDonnees::getInstance() {
if (_instance == 0) {
_instance = new Etat_SansDonnees() ;
}
return _instance ;
}

II-C-4 - Changements d'état

Dans les différentes méthodes des états, on trouve le paramètre sender, du type de l'objet appelant la méthode. Cela permet de spécifier le prochain état de l'objet appelant (c'est de cette manière qu'on implémente les transitions).

Exemple :

fichier .cpp

int Etat1::executer( Pricipale * sender ) {

...

//  On passe à l'état 2
sender->setState( Etat2::getInstance() ) ;

// Le changement d'état n'est effectif qu'au prochain return

...
}

Les transitions (=changements d'états) peuvent avoir trois origines :

1. Les états eux-mêmes (cas des automates)
Dans le cas d'automates, l'exécution est entièrement déléguée au sein même des différents états. Ce sont donc les états qui doivent effectuer eux-mêmes les transitions (c'est pour cela qu'ils gardent une référence sur la classe appelante dans le prototype de leurs méthodes) ;

2. La classe appelante
Les méthodes de la classe appelante peuvent commander elles-mêmes le changement d'état (dans ce cas, seule une partie de l'exécution est déléguée dans les différents états, les tâches importantes étant réalisées par la classe appelante) ;

3. Des éléments extérieurs (autres objets, utilisateur)
Certains éléments extérieurs peuvent demander explicitement le changement d'état d'un objet au travers de la méthode publique setState().

II-D - Exemples

Nous prendrons deux exemples, afin d'illustrer :

1. Les états sans données intrinsèques ;

2. Les états avec données intrinsèques.

II-D-1 - Automate pour protocole réseau

On peut définir un protocole réseau sous la forme d'un automate pour lequel :

- un état correspond à une action particulière à effectuer
(ex. : envoyer des paquets de données, attendre/envoyer un message d'Acknowledgement…) ;

- un événement est un fait extérieur au système qui modifie l'état de celui-ci
(ex. : messages reçus, timeout, erreurs réseau…).

Chaque événement donne lieu à une transition.

États et transitions

L'automate correspondant au protocole réseau sert alors uniquement à connaître l'état dans lequel se trouve l'application. Il n'y a pas de données propres aux états (sauf cas très particuliers). Il s'agit donc principalement d'un automate dont les états sont sans données intrinsèques.

II-D-2 - Un parseur SAX clair et efficace

SAX (Simple API for XML) est une API permettant le traitement de fichiers XML sous forme de flux, chaque élément (balises et autres) apparaissant dans le fichier lu déclenche un événement particulier. C'est l'implémentation des méthodes correspondant à ces événements (dans le SaxContentHandler) qui définit le comportement de l'application.

Un parseur SAX

L'avantage, c'est que cela permet de traiter des fichiers XML de manière efficace (sous forme de flux, très rapide, très peu de mémoire utilisée). L'inconvénient majeur, c'est que l'application a « la tête dans le guidon »: les événements arrivent les uns après les autres et il est nécessaire de sauvegarder le contexte dans lequel on se trouve pour définir le comportement de l'application, ce qui requière un effort de programmation plus important.

Dans ce contexte, le pattern Etat permet de simplifier l'implémentation d'un parseur SAX en nous fournissant un moyen de sauvegarder le contexte d'exécution et de modifier dynamiquement le comportement du parseur SAX en fonction des balises rencontrées.

Le but ici est de profiter des performances offertes par SAX dans le traitement des fichiers XML tout en permettant une implémentation souple, compréhensible, facile à maintenir et à corriger.

Je ne suis pas en train de dire que SAX est la « killer app », une solution miracle dans le domaine du traitement de fichiers XML et que l'on peut se passer de toutes les autres (DOM, XSLT…). Il y a des cas où SAX n'est pas applicable. Pour les cas où l'on peut utiliser SAX (traitement des fichiers XML en tant que flux), le pattern Etat peut faciliter l'implémentation et la maintenance.

L'idée est extrêmement simple: le SAXContentHandler délègue son comportement dans différents états. Ces états sont mis dans une pile et ce sont les méthodes de l'état situé au sommet de la pile qui sont appelées. Cela permet de modifier le comportement en ajoutant ou en enlevant des états en sommet de pile.

Finalement, on a donc un StackContentHandler qui intègre les mécanismes de gestion de la pile (push et pop). Ce StackContentHandler fait appel aux StackableContentHandler qui sont mis dans la pile et représentent les différents états (comportement) du parseur.

Un parseur SAX avec états empilables

Chaque état possède des données propres, relatives au traitement de la balise (ou du groupe de balises) en cours. Ces données peuvent être de toutes sortes (la liste des attributs de la balise, des compteurs, des valeurs lues, etc.). Le fait de disposer les états dans une pile permet :

1. D'avoir accès aux données des états dans la pile ;

2. De revenir au contexte précédent en fin de traitement d'une balise, par simple dépilement.

Ce principe de pile dans SAX est tellement simple et pratique que je me demande pourquoi il ne fait pas partie de l'implémentation de base de SAX. J'ai mis en place ce mécanisme au sein d'un projet permettant le traitement de fichiers XML de taille importante et nécessitant une implémentation suffisamment claire pour être à la fois maintenable et évolutive. Ce fut un succès.

Dans cet exemple, on empile différents états afin de sauvegarder le contexte d'exécution (ici: les balises parentes de la balise en cours de traitement). Ces états sont donc le plus souvent porteurs d'informations propres (liées au contexte d'exécution). Il s'agit donc bien d'un cas d'état avec données intrinsèques.

Note : ce qui est décrit dans ce paragraphe (pour SAX) est sans doute également applicable pour StAX, même si je ne l'ai jamais mis en œuvre.

Section suivante: Développer avec le pattern Etat

III - Développer avec le Pattern Etat

Cette section montre comment le pattern Etat trouve sa place dans les différentes étapes du cycle de vie d'une application (comment s'intègre-t-il dans les spécifications, l'implémentation ? Quelle est son influence sur les tests et la maintenance ?).

III-A - Conception

Dans la phase de conception, on recense les différents états du système, ainsi que les transitions possibles entre ces différents états. On précise quel est l'état initial et quels sont les états finaux (c'est-à-dire les états pouvant terminer normalement l'exécution de l'application, sans retour de code d'erreur). On obtient ainsi le schéma d'un automate.

Un exemple d'automate

Un tel schéma est extrêmement facile à comprendre et a donc tout à fait sa place dans un rapport d'analyse. Il permet également de repérer très tôt les éventuelles erreurs de conception (ex-: problèmes de protocole) et, connaissant les types d'événements possibles, il permet de faire une liste exhaustive des différents comportements du système.

En complément du schéma, on documente les différents états et transitions :

Pour les états :

- nom de l'état,

- description de l'état, son rôle, ce qui s'y passe,

- spécifier s'il est final ou pas. S'il n'est pas final, spécifier les types d'erreurs retournés,

- spécifier les conditions d'invariant (voir plus loin) ;

Pour les transitions :

- spécifier les événements déclencheurs de la transition,

- spécifier (si applicable) les actions à effectuer lors de la transition (au début ou en fin de transition).

Les conditions d'invariant sont les conditions qui doivent être respectées tout au long de l'exécution d'un programme. Dans le cas des états, une condition d'invariant s'exprimera de la manière suivante: « si le système se trouve dans l'état EN, alors il vérifiera telles propriétés » (exemple de propriété: « la boite de dialogue est désactivée »).

III-B - Réalisation

Dans la phase de réalisation, il s'agit de traduire ce qui a été spécifié dans la phase de conception :

- à un état correspond une classe ;

- implémenter les changements d'état, comme décrit dans la section « implémentation ».

Cette phase est donc assez mécanique.

III-C - Test

Dans la phase de test, il s'agit de vérifier (entre autres) :

- que les comportements du système correspondent bien au schéma ;

- que les conditions d'invariant sont respectées pour chaque état ;

- que les transitions s'effectuent correctement ;

- que les changements d'états n'occasionnent pas de « fuite mémoire » (c'est-à-dire que les objets Etat sont bien supprimés s'ils ne servent plus).

Pour cela, on utilise :

1. Des revues de code ;

2. Des tests permettant de couvrir l'ensemble des chemins du schéma (=tests de recouvrement) ;

3. Des tests permettant de vérifier les conditions d'invariant état par état.

Pour élaborer les tests, on s'appuie sur les documents rédigés lors de la phase de conception (schémas + explications complémentaires: description des états, des transitions, de l'invariant…)

III-D - Évolutivité

Tôt ou tard (généralement plus tôt que prévu), l'application sera amenée à être maintenue ou à évoluer pour s'adapter à de nouveaux besoins. Dans ce contexte, l'utilisation du pattern Etat se révèle incroyablement souple, et faire évoluer l'application se résume souvent à :

- l'ajout de nouveaux états et transitions ;

- la modification des états et transitions existants.

Parfois, il s'agit d'ajouter de nouvelles fonctionalités, et donc de nouvelles méthodes au sein des différents états. Dans ce cas, on commence par déclarer une méthode virtuelle dans la super-classe Etat (celle au sommet de la hiérarchie) avant d'implémenter les comportements spécifiques à chaque état dans les sous-classes.

La super-classe peut éventuellement fournir une implémentation par défaut, l'important étant d'éviter les copier/coller.

Section suivante : Bonnes pratiques de conception (organisation en modules…)

IV - Bonnes pratiques de conception

Voici quelques conseils pour une bonne utilisation du pattern Etat.

IV-A - Proscrire les copier/coller

Il peut arriver que plusieurs états aient un comportement identique (ou très similaire) pour une ou plusieurs de leurs méthodes. Pourtant, en aucun cas il ne doit y avoir de copier/coller dans un programme (c'est le signe d'une mauvaise conception et la source de nombreux problèmes).

Pour éviter les copier/coller, il existe plusieurs techniques (mettre le code commun dans une méthode/fonction spécifique, utiliser le pattern Template Method, etc).

Il est déconseillé de partager le code commun à plusieurs états par simple héritage. Rien ne dit en effet que vous arriverez à organiser vos classes selon une hiérarchie logique. De plus, cela nuit à l'évolutivité de votre application (s'il existe des contraintes liées à la hiérarchie des classes, on ne peut plus ajouter de nouveaux états comme on le souhaite).

On peut également s'appuyer sur d'autres patterns tels que Strategy.

IV-B - Organisation en modules

Pour éviter les collisions de noms, il est nécessaire d'organiser les différents états en modules. Pour ce faire, on utilisera les spécificités du langage de programmation utilisé (namespace, package…).

En pratique, la super classe Etat fait partie intégrante de l'interface de la classe principale et fait donc partie du même module (namespace ou package). Ce sont les classes héritant de la classe Etat qui doivent être définies dans un sous-module :

Organisation en modules

En C++

En C++, on met les différents états de la classe dans un namespace à part.

À ce titre, il est vivement recommandé d'utiliser une convention de nommage pour les différents namespace et de s'y tenir. On pourra par exemple prendre le mot « Etat » et lui apposer le nom de la classe principale pour définir le namespace. (ex-: Etat_MaClasse )

On aura ainsi, dans le fichier .h :

namespace Etat_UneClasse {

// définition des différents états de UneClasse

}

et dans le fichier .cpp implémentant le comportement des états, afin de ne pas répéter le namespace à chaque fois :

using namespace Etat_UneClasse ;

En Java

En Java, il existe deux façons de faire :

- utiliser les packages ;

- utiliser les classes internes (nested classes).

Sur le principe, les packages Java sont comparables aux namespaces du C++. On aura alors 1 fichier pour chaque classe définie (certains environnements autorisent la définition de plusieurs classes dans un même fichier, mais ce n'est pas très propre). Concrètement, cela veut dire que le développeur se retrouvera souvent avec plusieurs fichiers d'ouverts en même temps: un pour chaque état qu'il est en train de regarder, mais c'est un moindre mal avec l'amélioration des IDE.

Une classe interne, c'est une classe définie à l'intérieur d'une autre :

class UneClasse {
...

class UneClassInterne {
...
}
}

Ceci est une spécificité de Java.

Voir ce lien: nested classes (site de Sun)

Dans ce cas, les différents états seront définis dans le même fichier, et plus précisément à l'intérieur de la même classe, ce qui ne facilite pas la visibilité et rend l'ajout de nouveaux états plus compliqué.

L'utilisation de classes internes peut éventuellement se justifier dans le cas de classes ne comprenant que très peu d'états, que ces états soient simples et quasi « immuables » (non sujets à des changements fréquents dans l'implémentation). Dans l'immense majorité des cas, on aura recours aux packages.

Sections suivantes: Discussion autour du pattern Etat (avantages, inconvénients, applications)

V - Avantages et inconvénients

Un résumé de ce que le pattern Etat peut vous apporter, ainsi que les précautions à prendre.

V-A - Avantages

Facilite la rédaction des rapports

Grâce au pattern Etat, les rapports deviennent plus clairs et s'enrichissent de schémas compréhensibles. Les descriptions des différents états de l'application deviennent plus précises (on dispose pour chacun des états d'une description détaillée de ce qu'il s'y passe).

Améliore la qualité globale des applications

Lors de la phase de conception, le schéma permet de suivre les différentes étapes d'exécution du programme et de détecter très tôt les erreurs de conception (ex-: problème de protocole réseau). Dans les phases d'implémentation et de test, pour chaque état on dispose des conditions d'invariant qui doivent être respectées lorsque l'application se trouve dans cet état. Il devient alors très simple d'élaborer des tests pour savoir si ces conditions sont respectées. Enfin, grâce au pattern Etat, l'implémentation devient plus claire et facile à tracer. Tout ceci contribue largement à l'amélioration globale de la qualité de l'application développée (surtout dans le cas de grosses applications).

Facilite la maintenance et améliore l'évolutivité

Il est très facile de tracer l'application utilisant des états. Il est également extrêmement facile de rajouter de nouveaux états et de modifier certaines transitions pour adapter l'application à de nouveaux besoins. Ceci rend l'application plus évolutive et facile à maintenir.

V-B - Inconvénients

Multiplication des classes

Chaque état nécessite une classe, et donc tout autant de déclarations, d'implémentation de constructeurs, de destructeurs, etc. En contrepartie, le code est très bien organisé et compartimenté en classes et il devient facile de retrouver les informations relatives à un comportement (ou de localiser un bug).

Nécessite un temps d'adaptation

La programmation avec des états diffère sensiblement de la programmation « classique » et utilise des mécanismes particuliers (toutes les opérations sont déléguées à l'état courant, qui est lui-même chargé de modifier les variables de l'objet appelant). Cela demande un petit temps d'adaptation. Une fois que l'on a compris comment tout cela fonctionne, on est plus réceptif aux bénéfices que l'on peut tirer de ces techniques. La difficulté réside parfois à faire passer le message à ses collègues. Persévérez !

Section suivante: Domaines d'application du pattern Etat

VI - Applications

Quelques exemples d'applications du pattern Etat (il y en a surement d'autres auxquelles je n'ai pas pensé !).

VI-A - Définition et implémentation de protocoles

Voir l'exemple de l'automate pour le protocole réseau.

Dans cet exemple, le pattern Etat permet l'implémentation simple de protocoles réseau. Le fait d'exprimer des protocoles réseau au moyen d'automates permet de vérifier lors de la spécification si le protocole ne comporte pas de failles (sécurité, livelocks…) et de s'assurer, lors de l'implémentation, que le protocole a été implémenté conformément aux spécifications.

VI-B - Interface utilisateur

Le pattern Etat peut se révéler utile dans l'implémentation d'interfaces utilisateur, en particulier les interfaces pour lesquelles il existe plusieurs « modes » (et selon le mode en cours, les actions des différentes commandes se trouvent modifiées).

À chaque mode correspond alors un état. Lorsqu'une commande est activée, la méthode correspondante dans l'état en cours est appelée. Il est ainsi possible de désactiver temporairement certaines commandes en implémentant une méthode qui ne fait rien.

VI-C - Traitement de fichiers

Voir l'exemple du traitement de fichiers XML avec SAX.

Dans cet exemple, l'empilement des états a permis la sauvegarde et la restauration des différents contextes d'exécution durant les différentes phases du traitement du fichier.

VI-D - Intelligence Artificielle

Le pattern Etat peut servir dans l'implémentation de « bots », c'est-à-dire de programmes reproduisant le comportement humain. Par exemple dans un jeu vidéo, un robot pourra avoir différents états (« attaquer », « aller en renfort », « défendre la position », « rejoindre un point stratégique »…). Suivant le mode (l'état) dans lequel il est, le robot réagira différemment aux différents stimuli du monde qui l'entoure. En interprétant ces stimuli, le robot pourra décider de changer d'état.

Une telle implémentation est à rapprocher du pattern Strategy.

Section suivante: Conclusion

VII - Conclusion

Dans cet article, nous avons détaillé les principes de fonctionnement du pattern Etat. Nous avons également vu plusieurs exemples d'utilisations possibles du pattern Etat, dans des domaines très différents de l'informatique.

Bien que différentes, ces applications possèdent un dénominateur commun: dans tous les cas, le pattern Etat a permis la décomposition d'un système dont l'exécution est parfois complexe en plusieurs états simples à comprendre. C'est là que réside tout l'intérêt du pattern Etat.

En ayant recours au pattern Etat, il devient possible :

- de donner une représentation graphique de l'exécution du système (sous forme d'un automate) ;

- de documenter précisément chacun des états du système (actions réalisées, comportement attendu, conditions d'invariant, transitions possibles, etc.) ;

- de vérifier visuellement (voire prouver formellement) que le système ne comporte pas d'incohérences ;

- d'élaborer des stratégies de test (tests unitaires: vérifier que le comportement de chaque état est conforme aux spécifications; test de recouvrement: élaborer une série de tests permettant de vérifier en un temps optimal chacune des transitions du système) ;

- de faciliter la localisation des bugs (car 1 comportement = 1 état = 1 classe) en vue de les corriger ;

- d'obtenir une promotion. En effet, en présentant des rapports détaillés, clairs, précis, richement illustrés de schémas et en mettant en place une application robuste, facile à maintenir et évolutive, vous augmentez vos chances d'attirer les faveurs de votre patron qui pourrait bien vous accorder l'augmentation que vous lui réclamez depuis si longtemps… ;-)

Cet article a également permis de montrer comment l'utilisation du pattern Etat pouvait s'intégrer dans le processus de développement (des spécifications aux tests), faciliter la maintenance et permettre une certaine évolutivité.

VIII - Remerciements

Merci à Miles, Nip, christopheJ, Ricky81, wichtounet et hiko-seijuro pour la relecture de cet article.

IX - Téléchargement

Cet article au format PDF