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 :
- Un ensemble fini d'états :
DRAFT,ENRICHED,PUBLISHED... - Un ensemble de transitions : les passages autorises d'un état a un autre
- Des guards (gardes) : les conditions a remplir pour qu'une transition soit autorisee
- 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