05 - Transitions, guards et side effects
Ce que tu vas apprendre
- Ce qu'est une transition et ses propriétés
- Ce qu'est un guard et comment il protégé le cycle de vie
- Ce qu'est un side effect et quand il se déclenché
- La différence entre une transition implicite et explicite
Prerequisites
La transition : le passage d'un état a un autre
Une transition est le passage d'un état A a un état B. C'est l'unité de base du cycle de vie. Chaque transition a quatre propriétés :
Explicite
Elle a un nom. Pas "on met à jour le status", mais ENRICH, VALIDATE, PUBLISH. Le nom dit ce qui se passe en termes métier.
Contrainte
Elle n'est autorisee que si les guards (gardes) sont satisfaits. On ne peut pas transitionner "comme ca".
Atomique
Elle reussit complètement ou pas du tout. Il n'y a pas d'état "a moitie transitionne". Si le guard echoue ou si le side effect plante, on reste dans l'état d'origine.
Auditable
Elle laisse une trace : qui a déclenché la transition, quand, depuis quel état, vers quel état. C'est fondamental pour le debug et la conformité.
Le guard : la precondition
Un guard (garde) est une condition qui doit etre vraie pour qu'une transition soit autorisee. C'est le gardien du cycle de vie.
Exemples concrets :
- Pour passer de
DRAFTaENRICHED: "la Place doit avoir au moins une categorie ET au moins une traduction avec description" - Pour passer de
IMAGES_PROCESSEDaREADY_FOR_PUBLICATION: "la Place doit avoir au moins 3 images avec Top 1, 2 et 3 assignes" - Pour passer de
READY_FOR_PUBLICATIONaPUBLISHED: "les traductions EN et FR doivent avoir une description non vide"
typescript// Guard : une fonction pure qui retourne un booleen
function canEnrich(place: PlaceEntity): boolean {
return (
place.categories.length > 0 &&
place.translations.length > 0 &&
place.translations.every((t) => t.description !== null && t.description.length > 0)
);
}
// Guard pour la validation d'images
function canValidateImages(place: PlaceEntity): boolean {
const tops = place.images.filter((i) => i.top !== null);
return tops.length >= 3 && new Set(tops.map((i) => i.top)).size >= 3;
}
Propriétés d'un bon guard
- Fonction pure : pas de side effects, pas d'appels réseau, pas d'écriture en base
- Deterministe : pour les memes donnees, toujours le meme résultat
- Testable unitairement : un guard se teste en une ligne
- Independant du contexte : il regarde l'entité, pas "qui" demande la transition
Le side effect : l'action déclenchée
Un side effect (effet de bord) est une action qui se produit apres qu'une transition a reussi. C'est tout ce qui n'est pas le changement d'état lui-meme.
Exemples :
- Envoyer un email de notification quand une Place passe a
PUBLISHED - Mettre à jour un cache quand une Place passe a
ENRICHED - Logger la transition pour l'audit trail
- Declencher le pipeline d'images quand on passe a
READY_FOR_IMAGES
typescriptconst transitions = {
READY_FOR_PUBLICATION: {
PUBLISH: {
target: "PUBLISHED",
guard: canPublish,
sideEffects: [
(place) => auditLog.record("PUBLISH", place.id, Date.now()),
(place) => notificationService.send(`Place ${place.name} publiee`),
(place) => cacheService.invalidate(`place:${place.id}`),
],
},
},
};
Regle importante
Les side effects se declenchent apres le changement d'état, jamais avant. Si un side effect echoue, l'état a deja change. C'est pour ca qu'on les concoit pour etre idempotents (on verra ca dans l'article 08) : si on les re-exécuté, ca ne casse rien.
Mauvais exemple : transitions implicites
Voici ce que ca donne sans transitions explicites :
typescript// Mauvais : transition implicite dans du code imperatif
async function handlePlaceUpdate(place: Place, data: UpdateData) {
place.name = data.name;
place.categories = data.categories;
// La transition est cachee dans un if/else
if (data.categories.length > 0 && data.translations.length > 0) {
place.status = "ENRICHED"; // Qui a autorise ca ? Quel guard ? Aucune trace.
}
await db.save(place);
}
Problèmes : pas de guard formel, pas de trace, pas de validation que la transition est autorisee depuis l'état actuel. Si place.status etait PUBLISHED, on vient de le repasser a ENRICHED sans aucun contrôle.
Bon exemple : transitions declaratives
typescript// Bon : chaque transition est declaree, gardee, tracee
async function enrichPlace(placeId: string, enrichmentData: EnrichmentData) {
const place = await db.findById(placeId);
// La transition map sait que DRAFT -> ENRICHED est autorise
// Le guard verifie les preconditions
// Le side effect logge la transition
const updated = transition(place, "ENRICH");
await db.save(updated);
}
La logique de "qui peut transitionner ou" est séparée de la logique de "comment on fait la transition". C'est ca, la séparation des responsabilités.
Résumé
- Une transition est explicite, contrainte, atomique et auditable
- Un guard est une precondition pure et testable qui protégé une transition
- Un side effect est une action déclenchée apres une transition reussie
- Les transitions implicites (if/else dans du code métier) sont la source de bugs et d'états incoherents
- Les transitions declaratives (transition map) rendent le système lisible, testable et sur
Article précédent : 04 - La machine a états Article suivant : 06 - Single Source of Truth (SSOT)