Blog

Pourquoi écrire du code de machine à états à la main est une mauvaise idée

Publié le 2026-05-26

Pourquoi écrire du code de machine à états à la main est une mauvaise idée

Dans les projets de logiciels embarqués, la logique la plus critique et la plus complexe vit généralement dans une machine à états. Contrôleur de moteur, protocole de communication, flux d'interface utilisateur, gestion de l'énergie — tous répondent à la même question : « Dans quel état suis-je en ce moment, et comment dois-je réagir à chaque événement ? »

Et la plupart des équipes répondent à cette question de la même façon : blocs switch/case, définitions d'enum, variables d'état, chaînes de if. Ça fonctionne. Un temps.


Un scénario familier

Imaginons un contrôleur de porte de véhicule. La première version comporte quatre états : Ouvert, EnFermeture, Fermé, EnOuverture. Le code fait 200 lignes — lisible, testable.

Six mois plus tard, les exigences changent :

  • Mode automatique ajouté — la porte doit se fermer d'elle-même après un délai défini
  • Mode manuel ajouté — l'utilisateur peut désactiver le comportement automatique
  • Le basculement entre les deux modes doit être possible à l'exécution
  • Le dernier état doit être mémorisé en cas de coupure de courant
  • Nouveau capteur ajouté — les positions complètement ouverte et complètement fermée doivent être détectées séparément

Le code fait désormais 800 lignes. Chaque bloc switch examine chaque état. La variable de mode isAutomatic est évaluée dans chaque condition. Un nouveau développeur passe une semaine à essayer de comprendre le code.

C'est l'explosion d'états — le destin inévitable des machines à états écrites à la main.


Les coûts réels

Le coût du code de machine à états écrit à la main est rarement mesuré avec précision, car il s'accumule progressivement.

Coût de maintenance. L'ajout d'un nouvel état ou d'une nouvelle transition oblige à toucher tous les blocs switch. Il est facile de modifier un endroit et d'en oublier un autre. Le compilateur ne le détectera pas.

Coût de test. Chaque combinaison état–événement doit être testée. 10 états × 10 événements = 100 scénarios de test. Les écrire et les maintenir à jour à la main représente un effort considérable. Dans la plupart des projets, cela ne se fait tout simplement pas.

Coût d'intégration. Quand un nouveau développeur consulte le code pour la première fois, il n'y a pas de modèle — seulement du code. Il doit reconstruire l'intention de conception à partir de l'implémentation. Il n'y parvient que rarement complètement, et finit par contourner la logique existante plutôt que de l'étendre proprement.

Coût de synchronisation. Même lorsqu'un document de conception existe, le maintenir synchronisé avec le code constitue une charge de travail à part entière. Dans la plupart des projets, les deux divergent avec le temps.


La racine du problème

Le problème n'est pas l'utilisation de switch/case. Le problème est celui-ci : le modèle et l'implémentation vivent séparément.

Un concepteur dessine un diagramme. Un développeur traduit ce diagramme en code. Il n'existe aucun lien automatique entre les deux. Le diagramme change, le code change — indépendamment l'un de l'autre.

Dans les machines à états hiérarchiques, cela devient encore plus difficile à gérer. États imbriqués, pseudo-états d'historique, régions parallèles — modéliser correctement tout cela avec des switch/case en C++ est à la fois complexe et sujet aux erreurs.


Ce que l'approche basée sur le modèle change

UMTSM aborde ce problème sous un angle différent : dériver à la fois le modèle et le code de la même source.

Le développeur définit la machine à états dans un fichier .umt. À partir de cette définition :

  • Le squelette de la machine à états C/C++ est généré automatiquement
  • Les fixtures de test et les mocks sont générés automatiquement
  • La configuration de build CMake est générée automatiquement

Quand le modèle change, le code généré change. Les implémentations utilisateur (code d'action) sont préservées. La conception et l'implémentation ne divergent jamais.

Dans l'exemple du contrôleur de porte, les cinq changements d'exigences ont tous été répercutés dans le fichier .umt :

persistent deep history -> ManualMode_Open;

state AutomaticMode
{
    state Open
    {
        entry / resetWaitingTime;
        do    / wait;                 // ← thread du minuteur
        -> Closing;                   // ← transition automatique à la fin

        Manual -> ManualMode:Open;    // ← changement de mode en une ligne
    }
}

Ces sept lignes expriment :

  • À l'entrée dans l'état Open, le minuteur est réinitialisé
  • do / wait s'exécute comme un thread et se termine à l'expiration du délai
  • À la fin, une transition vers Closing est déclenchée
  • À la réception de l'événement Manual, une transition vers ManualMode est déclenchée

Le code C++ généré implémente cette sémantique de manière correcte et déterministe — protection par mutex, gestion des threads et restauration de l'historique comprises.


« Mais nous avons déjà du code qui fonctionne »

La plupart des projets arrivent à ce point et déclarent : « La migration n'en vaut pas la peine. » Je comprends — modifier du code fonctionnel comporte des risques.

Mais la bonne question est : quel est le coût de maintenance du code existant ?

Combien de temps prend chaque nouvelle fonctionnalité ? Combien de semaines faut-il pour intégrer un nouveau développeur ? La couverture de test est-elle véritablement suffisante ? Combien de fichiers un seul changement d'exigence touche-t-il ?

Les réponses à ces questions révèlent généralement le véritable coût d'un code de machine à états écrit à la main, sans modèle sous-jacent.


Conclusion

Écrire du code de machine à états à la main n'est pas une erreur — mais cela ne passe pas à l'échelle. Plus le système grandit, plus les exigences évoluent et plus l'équipe s'agrandit, plus l'écart entre modèle et implémentation se creuse — il ne se referme pas, il s'élargit.

La valeur de l'approche basée sur le modèle tient à ceci : l'intention de conception vit dans le code source, la testabilité est garantie structurellement, et un nouveau développeur comprend le système en lisant le fichier .umt.

UMTSM a été développé pour apporter cette approche aux projets C/C++ embarqués. Dans le prochain article, nous examinerons comment le code généré est testé et comment les fixtures de test générées automatiquement sont utilisées.


Pour toute question ou tout projet lié à UMTSM, n'hésitez pas à prendre contact.