07 - Les invariants : ce qui doit toujours etre vrai
Ce que tu vas apprendre
- Ce qu'est un invariant et pourquoi c'est central
- Les propriétés d'un bon invariant
- Comment les invariants se lient aux états du cycle de vie
- Comment tester les invariants
Prerequisites
Qu'est-ce qu'un invariant ?
Un invariant est une condition qui doit toujours etre vraie quand une entité est dans un état donne. C'est un contrat : "si tu me dis que cette Place est PUBLISHED, alors je te garantis que..."
Exemples :
- "Une Place en état
PUBLISHEDdoit avoir au moins 3 URL d'image valide" - "Une Place en état
ENRICHEDdoit avoir au moins une categorie et une traduction avec description" - "Une Place en état
IMAGES_PROCESSEDdoit avoir les Top 1, 2 et 3 assignes a trois images distinctes"
Un invariant n'est pas un souhait. C'est un fait verifiable. Si l'invariant est faux, le système est dans un état incoherent et il y a un bug.
Les propriétés d'un bon invariant
1. Testable
Tu dois pouvoir écrire un test automatise qui vérifié l'invariant. Si tu ne peux pas le tester, ce n'est pas un invariant, c'est un commentaire.
typescript// Invariant testable
function invariant_PUBLISHED(place: PlaceEntity): boolean {
const validImages = place.images.filter((img) => img.url && img.url.startsWith("https://"));
return validImages.length >= 3;
}
// Test
test("une Place PUBLISHED a au moins 3 images valides", () => {
const place = buildPublishedPlace(); // factory de test
expect(invariant_PUBLISHED(place)).toBe(true);
});
2. Binaire
Un invariant est vrai ou faux. Pas "a peu pres", pas "dans la plupart des cas". C'est un booleen.
typescript// Bon : binaire
const isValid = place.images.length >= 3; // true ou false
// Mauvais : pas binaire
const quality = place.images.length / 3; // 0.66... c'est quoi ? Valide ? Pas valide ?
3. Independent de l'implementation
L'invariant decrit une regle métier, pas un détail technique. Il ne doit pas dépendre de comment le code est écrit.
typescript// Bon : regle metier
"Une Place PUBLISHED a au moins 3 images valides"
// Mauvais : detail d'implementation
"Le tableau place.images a un length >= 3 et chaque element a une propriete url non null"
La première formulation survivra a un changement d'implementation (par exemple, si les images passent d'un tableau a une Map). La deuxieme, non.
Un invariant par état
Chaque état du cycle de vie devrait avoir ses invariants. Voici ceux du domaine Place :
| État | Invariant |
|---|---|
DRAFT |
name non vide, googlePlaceId present |
ENRICHED |
Au moins 1 categorie, au moins 1 traduction avec description |
READY_FOR_IMAGES |
Toutes les traductions ont une description non vide |
IMAGES_PROCESSING |
scrapedImages.length > 0 |
IMAGES_PROCESSED |
Au moins 3 images générées, Top 1/2/3 assignes |
READY_FOR_PUBLICATION |
Images validees par un humain |
PUBLISHED |
Au moins 3 URL d'image valide, toutes les traductions completes |
Ces invariants sont cumulatifs : un invariant de PUBLISHED inclut implicitement tous les invariants des états précédents (puisqu'on ne peut arriver a PUBLISHED qu'en passant par tous les états d'avant).
Chaque bug est un invariant casse
C'est une regle puissante : tout bug peut etre decrit comme un invariant qui a ete viole.
- "La Place est publiee mais elle n'a pas d'images" --> l'invariant de
PUBLISHEDest casse - "La Place est marquee ENRICHED mais n'a aucune categorie" --> l'invariant de
ENRICHEDest casse - "La Place est en IMAGES_PROCESSED mais les Tops ne sont pas assignes" --> l'invariant de
IMAGES_PROCESSEDest casse
Cette facon de penser transforme le debug : au lieu de chercher "ou est le bug dans le code", tu cherches "quel invariant a ete viole, et quelle transition l'a permis".
Comment tester les invariants
Verification a la transition
Le meilleur moment pour vérifier un invariant, c'est juste apres une transition. Si on vient de passer a ENRICHED, on vérifié l'invariant de ENRICHED :
typescriptfunction transition(place: PlaceEntity, event: string): PlaceEntity {
// ... logique de transition ...
const updated = { ...place, status: targetState };
// Verifier l'invariant du nouvel etat
const invariantCheck = invariants[targetState];
if (invariantCheck && !invariantCheck(updated)) {
throw new Error(`Invariant viole pour l'etat ${targetState}`);
}
return updated;
}
Verification periodique
Tu peux aussi vérifier les invariants de facon periodique (un cron job, par exemple) pour détecter les incoherences qui auraient echappe aux guards :
sql-- Trouver les Places PUBLISHED sans images
SELECT id, name FROM places
WHERE status = 'PUBLISHED'
AND id NOT IN (SELECT place_id FROM images GROUP BY place_id HAVING COUNT(*) >= 3);
Résumé
- Un invariant est une condition qui doit toujours etre vraie dans un état donne
- Il est testable, binaire, et independant de l'implementation
- Chaque état du cycle de vie a ses propres invariants
- Tout bug est un invariant viole : ca change ta facon de debugger
- On vérifié les invariants a la transition et/ou periodiquement
Article précédent : 06 - Single Source of Truth (SSOT) Article suivant : 08 - Idempotence : exécuter sans crainte