Domaines et cycles de vie - 04 - La machine a états (State Machine / FSM)

Comment implementer une machine a états finis en TypeScript. Transition map declarative vs if/else, avec exemples concrets pour le backend.

04 - La machine a états (State Machine / FSM)

Ce que tu vas apprendre

  • Ce qu'est une machine a états finis (FSM) et sa définition formelle
  • Pourquoi c'est mieux que des chaînes de if/else
  • La différence entre gestion d'état declarative et imperative
  • Comment implementer une machine a états en TypeScript

Prerequisites


Définition formelle

Une machine a états finis (Finite State Machine, ou FSM) est un modèle mathematique qui definit :

  1. Un ensemble fini d'états : DRAFT, ENRICHED, PUBLISHED...
  2. Un ensemble de transitions : les passages autorises d'un état a un autre
  3. Des guards (gardes) : les conditions a remplir pour qu'une transition soit autorisee
  4. Des side effects (effets de bord) : les actions declenchees quand une transition a lieu

C'est exactement le cycle de vie qu'on a vu dans l'article précédent, mais formalise dans une structure de donnees.

+-------+  enrich()   +----------+  request_images()  +-----------------+
| DRAFT |------------>| ENRICHED |-------------------->| READY_FOR_IMAGES|
+-------+             +----------+                     +-----------------+
  etat                  etat          transition             etat
                          |
                          |  Guard: hasCategories &&
                          |         hasTranslations
                          |
                          |  Side effect: log("Place enrichie")

Pourquoi pas des if/else ?

Voici comment on gere souvent l'état sans state machine :

typescript// Approche imperative : if/else
function processPlace(place: Place) {
  if (place.categories.length > 0 && place.translations.length > 0) {
    if (place.images.length === 0) {
      place.status = "READY_FOR_IMAGES";
    } else if (place.images.length >= 3) {
      place.status = "READY_FOR_PUBLICATION";
    }
  } else {
    place.status = "DRAFT";
  }
}

Problèmes :

  • Pas lisible : il faut lire tout le code pour comprendre les transitions possibles
  • Pas testable : comment tester qu'on ne peut PAS passer de DRAFT a PUBLISHED directement ?
  • Pas extensible : ajouter un état oblige a modifier tous les if/else
  • Pas auditable : aucune trace de qui a fait quoi, quand
  • Bugs silencieux : une mauvaise condition et une entité se retrouve dans un état incoherent

L'approche declarative : la transition map

Avec une state machine, tu declares les transitions comme une structure de donnees :

typescript// Approche declarative : transition map
const transitions = {
  DRAFT: {
    ENRICH: {
      target: "ENRICHED",
      guard: (place) => place.categories.length > 0 && place.translations.length > 0,
      sideEffect: (place) => console.log(`Place ${place.id} enrichie`),
    },
  },
  ENRICHED: {
    REQUEST_IMAGES: {
      target: "READY_FOR_IMAGES",
      guard: (place) => place.translations.every((t) => t.description !== null),
    },
  },
  READY_FOR_IMAGES: {
    PROCESS_IMAGES: {
      target: "IMAGES_PROCESSING",
      guard: (place) => place.scrapedImages.length > 0,
    },
  },
  IMAGES_PROCESSING: {
    IMAGES_DONE: {
      target: "IMAGES_PROCESSED",
      guard: (place) => place.processedImages.length >= 3,
    },
  },
  IMAGES_PROCESSED: {
    VALIDATE: {
      target: "READY_FOR_PUBLICATION",
      guard: (place) => place.images.filter((i) => i.top !== null).length >= 3,
    },
  },
  READY_FOR_PUBLICATION: {
    PUBLISH: {
      target: "PUBLISHED",
      guard: (place) => place.images.length >= 3 && place.name !== "",
    },
  },
};

Avantages

  • Lisible : tu vois d'un coup d'oeil tous les états et toutes les transitions
  • Testable : tu peux vérifier chaque transition individuellement
  • Extensible : ajouter un état = ajouter une entree dans l'objet
  • Auditable : chaque transition est un événement qu'on peut tracer
  • Sur : une transition non déclarée est impossible

Le moteur de transitions

Pour utiliser cette transition map, il faut un petit moteur :

typescriptfunction transition(place: Place, event: string): Place {
  const currentTransitions = transitions[place.status];
  if (!currentTransitions) throw new Error(`Pas de transitions depuis ${place.status}`);

  const transitionDef = currentTransitions[event];
  if (!transitionDef) throw new Error(`Transition ${event} non autorisee depuis ${place.status}`);

  if (transitionDef.guard && !transitionDef.guard(place)) {
    throw new Error(`Guard non satisfait pour ${event} depuis ${place.status}`);
  }

  if (transitionDef.sideEffect) transitionDef.sideEffect(place);

  return { ...place, status: transitionDef.target };
}

Utilisation :

typescriptlet place = { id: "42", status: "DRAFT", categories: ["cafe"], translations: [{ description: "Un cafe" }] };
place = transition(place, "ENRICH");
// place.status === "ENRICHED"

XState : une librairie pour les state machines

Si tu ne veux pas coder ton propre moteur, XState est une librairie TypeScript specialisee dans les state machines. Elle offre :

  • Un format declaratif pour définir les machines
  • Un visualiseur graphique (tu peux voir ta machine sous forme de diagramme)
  • La gestion des guards, side effects, et états parallèles
  • Un support pour les machines hierarchiques (machines dans des machines)

Ce n'est pas obligatoire -- un objet transitions simple suffit souvent -- mais pour des cas complexes, XState est un excellent outil. C'est le genre d'automatisation qui fait gagner du temps sur le long terme, que ce soit sur paltemps.fr ou sur n'importe quel projet avec des workflows complexes.


Résumé

  • Une FSM definit formellement : états + transitions + guards + side effects
  • Les chaînes de if/else sont illisibles, non testables, et source de bugs
  • L'approche declarative (transition map) rend le lifecycle lisible, testable et extensible
  • Un petit moteur de transitions suffit pour utiliser la map
  • XState est une option pour les cas complexes

Article précédent : 03 - Le cycle de vie d'une entité Article suivant : 05 - Transitions, guards et side effects

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.