Améliorez vos logiciels avec le pattern Etat


précédentsommairesuivant

2. Le Pattern "Etat"

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

 
Sélectionnez

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



Voici le code détaillé :

Fichier .h
Sélectionnez

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

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

2.2. Différents types d'états

Il existe 2 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).

2.3. Implémentation

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

Le pattern Etat
Hiérarchie de classes



Voici le prototype de telles classes :

fichier .h
Sélectionnez

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) ;
  } ;

}

2.3.2. Etats 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
Sélectionnez

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() ;
  } ;

}

2.3.3. Etats 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
Sélectionnez

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
Sélectionnez

using namespace Etat_Principale ;


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

2.3.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
Sélectionnez

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 3 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().



2.4. Exemples

Nous prendrons deux exemples, afin d'illustrer :

  1. les états sans données intrinsèques
  2. les états avec données intrinsèques

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

Automate réseau
Etats 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.

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

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

Au final, 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.

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



Section suivante: Développer avec le pattern Etat




précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2006 Pierre Caboche. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.