Les machines d'état servent à modéliser le comportement dynamique d'un élément de modèle et, plus précisément, les
aspects dépendant d'événements du comportement du système (voir Concept :
Evénements et signaux). Les machines d'état servent surtout à définir un comportement dépendant d'un état ou
variant en fonction de l'état dans lequel se trouve l'élément de modèle. Pour les éléments de modèle dont le
comportement reste inchangé par rapport à leur état, les machines d'état n'ont pas besoin de décrire ce comportement
(ces éléments sont généralement des classes passives dont la mission première est de gérer des données). Les machines
d'état doivent notamment être employées pour modéliser le comportement de classes actives utilisant des événements
d'appels et de signaux en vue d'implémenter leurs opérations (comme transitions dans la machine d'état de la classe).
Une machine d'état se compose d'états liés par des transitions. Un état désigne la condition d'un objet dans lequel il
réalise certaines tâches ou attend un événement. Une transition est une relation entre deux états déclenchée par un
événement, exécutant certaines actions ou évaluations et entraînant un état de fin spécifique. Les éléments d'une
machine d'état sont illustrés à la figure 1.
Figure 1. Notation de machine d'état.
Un éditeur simple peut être affiché comme processeur d'état avec les états Vide, Attente d'une commande
et Attente d'un texte. Les événements Charger fichier, Insérer un texte, Insérer caractère
et Sauvegarder et quitter génèrent des transitions dans la machine d'état. La machine d'état pour l'éditeur est
illustrée à la figure 1 ci-dessous.
Figure 2. La machine d'état pour un éditeur simple.
Un état désigne la condition d'un objet dans lequel il réalise certaines tâches ou attend un événement. Un objet peut
conserver un état pour une durée déterminée. Un état possède plusieurs propriétés :
Nom
|
Chaîne de texte distinguant l'état d'autres états. Un état peut également être anonyme, à savoir qu'il
n'a pas de nom.
|
Actions d'entrée/sorties
|
Actions exécutées à l'entrée et à la sortie de l'état.
|
Transitions internes
|
Transitions gérées sans provoquer de changement d'état.
|
Sous-états
|
Structure imbriquée d'un état qui inclut des sous-états disjoints (actifs de façon séquentielle) ou
simultanés (actifs de façon simultanée).
|
Evénements différés
|
Liste d'événements non gérés dans cet état mais reportés et mis en file d'attente pour une gestion par
l'objet dans un autre état.
|
Comme illustré à la figure 1, il existe deux états spéciaux pouvant être définis pour la machine d'état d'un objet.
L'état initial indique le point de départ par défaut pour la machine d'état ou le sous-état. Un état initial est
signalé par un rond noir. L'état final indique l'aboutissement de l'exécution de la machine d'état ou la fin de l'état
intégré. Un état final est signalé par un rond noir entouré d'un cercle non rempli. Les états initial et final sont des
pseudo-états. Aucun ne présente les caractéristiques habituelles d'un état normal à part le nom. Une transition d'un
état initial à un état final peut posséder l'ensemble des fonctions, dont une condition de garde et une action, mais ne
pas avoir d'événement déclencheur.
Une transition est une relation entre deux états : elle indique qu'un objet dans le premier état réalise certaines
actions et passe à un second état lorsqu'un événement spécifié se produit et que des conditions déterminées sont
remplies. Au moment du changement d'état, le lancement de la transition est demandé. Tant que la transition n'est pas
lancée, l'état de l'objet est dit source ; après le lancement en revanche, il est dit cible. Une transition possède
plusieurs propriétés :
Etat source
|
Etat affecté par la transition : si un objet se trouve dans l'état source, une transition sortante peut
être lancée lorsque l'objet reçoit l'événement déclencheur de la transition et si la condition de garde
(le cas échéant) est remplie.
|
Déclencheur d'événements
|
Evénement rendant possible le lancement de la transition (à condition que sa condition de garde soit
remplie) lorsqu'il est reçu par l'objet dans l'état source.
|
Condition de garde
|
Expression booléenne évaluée lorsque la transition est déclenchée par la réception du déclencheur
d'événements. Si l'expression a la valeur True, la transition peut être lancée ; si elle a la valeur
False, la transition n'est pas lancée. S'il n'existe pas d'autre transition pouvant être déclenchée par
le même événement, celui-ci est perdu.
|
Action
|
Calcul atomique exécutable qui peut directement agir sur l'objet possédant la machine d'état et
indirectement sur d'autres objets que l'objet peut voir.
|
Etat cible
|
Etat actif après l'aboutissement de la transition.
|
Une transition peut avoir plusieurs sources, auquel cas elle représente le regroupement de divers états simultanés,
ainsi que plusieurs cibles, auquel cas elle symbolise une division vers divers états simultanés.
Dans le contexte d'une machine d'état, un événement est l'occurrence d'un stimulus pouvant déclencher une transition
d'état. Les événements peuvent être de signaux et d'appels, correspondre au temps qui passe ou à un changement d'état.
Un signal ou un appel peuvent comporter des paramètres dont les valeurs sont disponibles pour la transition, y compris
des expressions pour les conditions de garde et l'action. Une transition sans déclenchement est également possible :
elle n'a dans ce cas pas de déclencheur d'événements. Ces transitions, également qualifiées de transitions
d'achèvement, sont déclenchées de façon implicite quand leur état source a terminé sa tâche.
Une condition de garde est évaluée une fois que l'événement déclencheur de la transition s'est produit. Vous pouvez
avoir plusieurs transitions depuis le même état source et avec le même déclencheur d'événements, tant que les
conditions de garde ne se chevauchent pas. Une condition de garde est évaluée une seule fois pour la transition, au
moment où l'événement a lieu. L'expression booléenne peut faire référence à l'état de l'objet.
Une action est un calcul atomique exécutable : elle ne peut donc pas être interrompue par un événement et s'exécute
complètement. Elle fonctionne par conséquent à l'inverse d'une tâche, que d'autres événements peuvent interrompre. Les
actions peuvent inclure des appels d'opérations (au propriétaire de la machine d'état et d'autres objets visibles), la
création ou la destruction d'un autre objet, ou encore l'envoi d'un signal à un autre objet. Dans ce dernier cas, le
nom du signal est précédé du mot clé send.
Les actions d'entrée et les sorties permettent à la même action d'être distribuée chaque fois que l'état est activé ou
quitté, respectivement. Grâce à ces actions, l'opération est effectuée proprement, sans devoir explicitement appliquer
des actions sur chaque transition entrante ou sortante. Ces actions peuvent ne pas avoir d'arguments ou de conditions
de garde. Les actions d'entrée au niveau supérieur d'une machine d'état pour un élément de modèle peuvent posséder des
paramètres représentant les arguments que la machine reçoit à la création de l'élément.
Les transitions internes permettent aux événements d'être gérés dans l'état sans quitter ce dernier, ce qui évite le
déclenchement d'actions d'entrée ou les sorties. Les transitions internes peuvent posséder des éléments avec des
paramètres et des conditions de garde et correspondre principalement à des gestionnaires d'interruption.
La gestion des événements différés est reportée tant qu'un état dans lequel un événement n'est pas différé n'est pas
actif. Une fois cet état actif, l'occurrence de l'événement est déclenchée et peut provoquer des transitions comme s'il
venait de se produire. L'implémentation d'événements différés demande la présence d'une file d'attente interne
d'événements. Si un événement se produit mais est répertorié comme différé, il est placé dans la file d'attente. Les
événements sont retirés de cette file d'attente dès que l'objet passe à un état ne les mettant pas en différé.
Un état simple n'a aucune sous-structure. Un état doté de sous-états (états imbriqués) est dit composite. Les
sous-états peuvent être imbriqués à n'importe quel niveau. Une machine d'état imbriquée doit avoir au moins un état
initial et un état final. Les sous-états servent à simplifier les machines d'état complexes en montrant que certains
états sont uniquement possibles dans un contexte donné (état parent).
Figure 3. Sous-états.
A partir d'une source hors d'un état composite parent, une transition peut cibler l'état composite ou un sous-état. Si
la cible est l'état composite, la machine d'état imbriquée doit inclure un état initial auquel passe le contrôle une
fois l'état composite actif et après distribution de son action d'entrée (le cas échéant). Si la cible est l'état
imbriqué en revanche, le contrôle passe à lui après distribution de l'action d'entrée de l'état composite (le cas
échéant), puis de l'action d'entrée de l'état imbriqué (le cas échéant).
Une transition issue d'un état composite peut avoir comme source l'état composite ou le sous-état. Dans les deux cas,
le contrôle quitte d'abord l'état imbriqué (et sa sortie, le cas échéant, est distribuée), puis l'état composite (dont
la sortie, le cas échéant, est également distribuée). Une transition dont la source est l'état composite interrompt
principalement la tâche de la machine d'état imbriquée.
Sauf mention contraire, lorsqu'une transition passe à un état composite, l'action de l'état imbriqué revient à l'état
initial (excepté si la transition prend directement comme cible un sous-état). Les états historiques permettent à la
machine d'état de passer à nouveau au dernier sous-état actif avant de quitter l'état composite. La figure 3 offre un
exemple d'utilisation d'un état historique.
Figure 4. Etat historique.
Les machines d'état servent le plus souvent à modéliser le comportement d'un objet au cours de son cycle de vie. Elles
s'avèrent notamment nécessaires lorsque des objets ont un comportement dépendant de leur état. Les objets pouvant
posséder des machines d'état sont des classes, des sous-systèmes, des cas d'utilisation et des interfaces (pour
affirmer les états devant être respectés par un objet qui génère l'interface). Dans le cas de systèmes en temps réel,
les machines d'état sont également employées pour des capsules et des protocoles (pour affirmer les états devant être
respectés par un objet à l'origine du protocole).
Tous les objets ne requièrent pas des machines d'état. Si le comportement d'un objet est simple, en ce sens qu'il ne
fait que stocker et recevoir des données, il est invariable quel que soit l'état et sa machine d'état n'a guère
d'intérêt.
La modélisation de la durée de vie d'un objet implique trois éléments : l'indication des événements auxquels l'objet
peut répondre, la réponse à ces événements et l'impact du passé sur le comportement actuel. Cette modélisation demande
aussi de décider l'ordre dans lequel l'objet peut répondre de façon intelligible aux événements, en partant de l'heure
de création de l'objet et en continuant jusqu'à sa destruction.
Pour modéliser la durée de vie d'un objet :
-
Définissez le contexte pour la machine d'état, qu'il s'agisse d'une classe, d'un cas d'utilisation ou du système
dans son ensemble.
-
Si le contexte est une classe ou un cas d'utilisation, rassemblez les classes voisines, y compris les
classes parent et celles accessibles par des associations ou des dépendances. Ces classes voisines sont des
cibles candidates pour des actions et pour intégration à des conditions de garde.
-
Si le contexte est le système dans son ensemble, limitez-vous à un comportement du système et prenez en
compte la durée de vie des objets impliqués dans cet aspect. La durée de vie du système entier est trop
importante pour être un contexte pertinent.
-
Etablissez les états initial et final pour l'objet. S'il existe des préconditions ou des postconditions pour ces
états, définissez-les également.
-
Déterminez les événements auxquels l'objet répond. Vous les trouverez dans les interfaces de l'objet. Dans le cas
de systèmes en temps réel, ils peuvent aussi figurer dans les protocoles de l'objet.
-
De l'état initial à l'état final, indiquez les états de niveau supérieur dans lesquels l'objet peut se trouver.
Connectez ces états aux transitions déclenchées par les événements appropriés. Poursuivez en ajoutant ces
transitions.
-
Identifiez toutes les actions d'entrée ou les sorties.
-
Développez ou simplifiez la machine d'état à l'aide de sous-états.
-
Vérifiez que tous les événements déclenchant les transitions dans la machine d'état correspondent à ceux attendus
par les interfaces générées par l'objet. De la même façon, assurez-vous que tous les événements attendus par les
interfaces de l'objet sont gérés par la machine d'état. Dans le cas de systèmes en temps réel, effectuez des
vérifications similaires pour les protocoles d'une capsule. Enfin, veillez aux endroits où vous voulez
explicitement ignorer des événements (par exemple, des événements différés).
-
Vérifiez que toutes les actions dans la machine d'état sont prises en charge par les relations, méthodes et
opérations de l'objet parent.
-
Faites un suivi de la machine d'état en la comparant aux séquences d'événements attendues et leurs réponses.
Recherchez les états inaccessibles et ceux dans lesquels la machine reste bloquée.
-
Si vous réorganisez ou restructurez la machine d'état, assurez-vous que la sémantique reste inchangée.
-
Lorsque le choix se présente, utilisez la sémantique visuelle de la machine d'état au lieu d'écrire le code de
détail de transition. Par exemple, ne déclenchez pas une transition sur plusieurs signaux et servez-vous du code de
détail pour gérer le flux de contrôle différemment en fonction du signal. Utilisez des transitions distinctes
déclenchées par des signaux différents. Evitez dans le code de transition la logique conditionnelle masquant les
comportements supplémentaires.
-
Nommez les états en fonction de vos attentes ou de ce qui se produit dans chacun d'eux. Pour rappel, un état n'est
pas un point dans le temps, mais une période au cours de laquelle la machine d'état attend que quelque chose se
passe. Par exemple, attenteFin est un nom plus pertinent que fin et retarderTâche plus que délai. Ne nommez pas les
états comme s'il s'agissait d'actions.
-
Nommez tous les états et les transitions dans une seule machine d'état ; le débogage au niveau de la source est
ainsi simplifié.
-
Utilisez avec prudence des variables d'état (attributs servant à contrôler le comportement) et n'y recourez pas au
lieu de créer de nouveaux états. S'il y a peu d'états, que les comportements dépendent peu ou pas d'états et que
peu de comportements ou aucun sont simultanés ou indépendants de l'objet contenant la machine d'état, vous pouvez
employer des variables d'état. En revanche, si un comportement complexe et dépendant d'états est potentiellement
simultané ou si des événements devant être gérés peuvent se produire en dehors de l'objet contenant la machine
d'état, envisagez la collaboration de deux objets actifs ou plus (éventuellement définie comme composition). Dans
des systèmes en temps réel, un comportement complexe, dépendant d'états et simultané doit être modélisé avec une
capsule renfermant des sous-capsules.
-
S'il existe plus de 5 ± 2 états dans un même diagramme, pensez à utiliser des sous-états. Tout est question de bon
sens : dix états dans un pattern totalement standard peuvent être acceptables, mais deux états avec 40 transitions
entre eux doivent évidemment être repensés. Assurez-vous que la machine d'état est compréhensible.
-
Nommez les transitions pour ce qui déclenche l'événement et/ou ce qui se produit pendant la transition. Choisissez
des noms facilitant la compréhension.
-
Dans le cas d'un vertex de choix, demandez-vous si vous pouvez déléguer la responsabilité pour ce choix à un autre
composant, afin qu'il soit présenté à l'objet en tant qu'ensemble distinct de signaux sur lesquels agir (par
exemple, au lieu d'un choix dans msg->data > x) ou si l'émetteur ou tout autre intermédiaire prend la
décision et envoie un signal avec la décision explicite dans le nom du signal (par exemple, utilisez les signaux
estPlein et estVide au lieu de créer une valeur et de vérifier les données du message).
-
Formulez de façon descriptive la question à laquelle une réponse est donnée dans le vertex de choix, comme
yAtIlEncoreDeLaVie ou estIlPossibleDeSePlaindre.
-
Dans un objet donné, essayez de prendre des noms uniques de vertex de choix, pour les mêmes raisons que les noms de
transitions doivent aussi être uniques.
-
Y a-t-il des fragments de code ou des transitions excessivement longs ? Des fonctions doivent-elles être employées
à la place et les fragments de code courants sont-ils capturés en tant que fonctions ? Une transition doit
ressembler à du pseudocode de niveau élevé et respecter des règles de longueur identiques ou encore plus strictes
que les fonctions C++. Par exemple, une transition avec plus de 25 lignes de code est considérée trop longue.
-
Les fonctions doivent être nommées par rapport à leur action.
-
Prêtez une attention spéciale aux actions d'entrée et les sorties : il est particulièrement facile d'effectuer des
changements et d'oublier de changer ces actions en conséquence.
-
Les sorties peuvent servir à fournir des fonctions de sécurité, par exemple si elles visent à appliquer une
assertion : tel est le cas de la sortie de l'état allumerChauffage, qui éteint le chauffage.
-
En général, les sous-états doivent contenir deux états ou plus, sauf si la machine d'état est abstraite et sera
assortie de sous-classes de l'élément parent.
-
Les points de choix doivent être utilisés à la place de la logique conditionnelle dans les actions et les
transitions. Ces points sont facilement reconnaissables, alors que la logique conditionnelle au sein du code est
masquée et peut facilement vous échapper.
-
Evitez les conditions de garde.
-
Si l'événement déclenche plusieurs transitions, aucun contrôle ne détermine la condition de garde évaluée
en premier. Par conséquent, les résultats peuvent être imprévisibles.
-
Plusieurs conditions de garde peuvent avoir la valeur True, mais une seule transition peut en revanche être
suivie. Le chemin choisi peut être imprévisible.
-
Les conditions de garde n'étant pas visuelles, il est difficile d'en détecter la présence.
-
Evitez les machines d'état ressemblant à des graphiques de flux.
-
Ceci peut signaler une tentative de modélisation d'une abstraction qui n'existe pas vraiment, comme suit :
-
utilisation d'une classe active pour modéliser le comportement le plus adapté à une classe passive
ou de données ;
-
modélisation d'une classe de données à l'aide d'une classe de données et d'une classe active
étroitement liées (la classe de données a été utilisée pour transmettre les informations de type
mais la classe active contient la plupart des données à associer à la classe de données).
-
Cette mauvaise utilisation des machines d'état se détecte grâce aux symptômes suivants :
-
des messages envoyés à soi-même, essentiellement pour réutiliser du code,
-
quelques états avec beaucoup de points de choix,
-
dans certains cas, une machine d'état sans cycles. Ce type de machine est valide pour traiter des
applications de contrôle ou pour contrôler une séquence d'événements. Leur présence lors de
l'analyse symbolise généralement la dégénération de la machine d'état dans un graphique de flux.
-
Une fois l'incident identifié :
-
envisagez la division de la classe active en unités plus petites avec des responsabilités plus
distinctes ;
-
déplacez plus de comportements dans une classe de données associée à la classe active de l'incident
;
-
déplacez plus de comportements dans les fonctions de la classe active ;
-
établissez des signaux plus pertinents au lieu de vous fier aux données.
Une machine d'état abstraite doit recevoir plus de détails avant de pouvoir servir à des fins pratiques. Elle peut être
employée pour définir un comportement réutilisable générique, ensuite amélioré dans des éléments de modèle.
Figure 5. Une machine d'état abstraite.
Observez la machine d'état abstraite à la figure 5. La machine d'état simple illustrée est représentative du niveau le
plus abstrait du comportement (l'automate de contrôle) de différents types d'éléments dans des systèmes gérés par des
événements. Même s'ils partagent tous cette forme de niveau élevé, les divers types d'éléments peuvent présenter des
comportements détaillés complètement différents dans l'état En fonctionnement selon leur objectif. Par conséquent,
cette machine d'état serait probablement définie dans une classe abstraite servant de classe racine pour les
différentes classes actives spécialisées.
Définissons par conséquent deux améliorations distinctes de cette machine d'état abstraite à l'aide de l'héritage. Ces
deux améliorations (R1 et R2) sont illustrées à la figure 6. Par souci de clarté, nous avons dessiné en gris les
éléments hérités de la classe parent.
Figure 6. Deux améliorations de la machine d'état à la figure 5.
Les deux améliorations se distinguent clairement par leur mode de composition de l'état En fonctionnement et
d'extension de la transition de départ d'origine. Il va de soi que ces choix peuvent uniquement être effectués une fois
l'amélioration connue : ils sont donc impossibles avec une seule transition de bout en bout dans la classe abstraite.
La possibilité de poursuivre tant les transitions entrantes que celles sortantes est essentielle pour le type
d'amélioration décrit plus haut. Il peut sembler que les points d'entrée et les états finaux, associés aux transitions
de continuation, suffisent pour fournir cette sémantique. Malheureusement, ils sont insuffisants dans le cas de
plusieurs transitions distinctes devant être étendues.
Pour le pattern de comportement abstrait est requise une méthode de chaînage de deux segments de transition ou plus,
tous exécutés dans la portée d'une même étape d'achèvement. Les transitions passant à un état hiérarchique sont alors
divisées entre la partie entrante, se terminant en effet à la frontière de l'état, et une extension se poursuivant dans
l'état. De la même façon, les transitions sortantes émanant d'un état imbriqué de façon hiérarchique sont segmentées
entre une partie se terminant à la frontière de l'état parent et une autre se poursuivant de cette frontière à l'état
cible. Cet effet peut être obtenu en langage UML avec l'introduction du concept d'état de chaîne. La
modélisation s'effectue par un stéréotype (<<chainState>>) du concept d'état UML. Il s'agit d'un état dont
le seul objectif est de "chaîner" des transitions automatiques (sans déclenchement) à une transition entrante. Un état
de chaîne ne possède aucune structure interne, aucune action d'entrée, aucune tâche interne et aucune sortie. Il est
également dénué de transitions déclenchées par des événements. Cet état peut inclure n'importe quel nombre de
transitions. Il peut avoir une transition sortante sans événement déclencheur : cette transition se lance
automatiquement lorsqu'une transition entrante active l'état. L'objectif de l'état est de chaîner une transition
entrante à une transition sortante distincte. Entre la ou les transition(s) entrantes et la transition sortante
chaînée, l'une se connecte à un autre état dans l'état parent et l'autre se connecte à un autre état hors de l'état
parent. L'idée de l'introduction d'un état de chaîne consiste à séparer la spécification interne de l'état parent de
son environnement externe : il s'agit donc d'un concept d'encapsulation.
En réalité, un état de chaîne correspond à un état de passage permettant de chaîner une transition à une transition de
continuation spécifique. Si aucune transition de continuation n'est définie, la transition se termine dans l'état de
chaîne et une transition dans un état parent finit par se lancer pour faire avancer les choses.
L'exemple de segment de machine d'état à la figure 7 illustre des états de chaîne et leur notation. Les états de chaîne
sont représentés dans un diagramme de machine d'état par de petits cercles blancs dans l'état hiérarchique approprié
(cette notation est similaire aux états initiaux et finaux). Les cercles sont des icônes du stéréotype de l'état de
chaîne et sont généralement dessinés près de la frontière pour plus de commodité. En fait, une variation de notation
consisterait à les dessiner sur la frontière de l'état parent.
Figure 7. Etats de chaîne et transitions chaînées.
La transition chaînée dans cet exemple comprend trois segments de transition chaînée e1/a11-/a12-/a13. A la réception
du signal e1, la transition intitulée e1/a11 est prise et son action a11 exécutée, puis l'état de chaîne c1 est
atteint. Après cela, la transition de continuation entre c1 et c2 est prise puis, comme c2 est aussi un état de chaîne,
la transition de c2 à S21. Si tous les états le long de ces chemins ont des actions d'entrée et les sorties, la
séquence d'exécutions d'actions est la suivante :
-
sortie de S11
-
action a11
-
sortie de S1
-
action a12
-
action d'entrée de S2
-
action a13
-
action d'entrée de S21
Tout ceci est exécuté dans la portée d'une seule étape d'achèvement.
Il faut le comparer à la sémantique d'exécution d'actions de la transition e2/a2 directe, à savoir :
-
sortie de S11
-
sortie de S1
-
action a2
-
action d'entrée pour l'état S2
-
action d'entrée pour l'état S21
|