State machine, ou les transitions qu'on rend impossiblesState machine, or the transitions we make impossible
status est une chaîne, et n'importe quelle méthode lui assigne n'importe quelle valeur : on annule une commande expédiée, on rembourse ce qui n'a jamais été payé. La règle « une commande expédiée ne s'annule pas » vit en gardes éparses, supposée plus que vérifiée — jusqu'au jour où l'illégal arrive en prod. La machine à états ferme l'ensemble en états finis, déclare en un seul endroit les transitions permises, et en fait la seule porte : une transition absente n'est pas refusée, elle est irreprésentable. Le piège des deux côtés : une machine décorative qu'un setter public contourne, ou un statechart à douze états pour un booléen. La machine ne vaut que si elle est la seule porte.status is a string, and any method assigns it any value: you cancel a shipped order, you refund what was never paid. The rule 'a shipped order isn't cancellable' lives in scattered guards, assumed more than enforced — until the day the illegal reaches prod. The state machine closes the set into finite states, declares the permitted transitions in one place, and makes that the only door: an absent transition isn't refused, it's unrepresentable. The trap on both sides: a decorative machine a public setter bypasses, or a twelve-state statechart for a boolean. The machine is only worth it if it's the only door.
AudienceAudience
Dev dont un champ status: str est muté depuis dix endroits sans gardeDev whose status: str field is mutated from ten places with no guard
Un état stocké dans un champ libre. Quelles transitions devraient être impossibles ?State stored in a free field. Which transitions should be impossible?
status est une chaîne, et n'importe quelle méthode lui assigne n'importe quelle valeur. La commande a pourtant un cycle de vie réel : on ne livre pas une commande annulée, on ne rembourse pas ce qui n'a pas été payé. Ce sont des règles — mais elles ne vivent nulle part en un seul endroit. Elles sont éparpillées en gardes partielles, supposées plus que vérifiées. Ce qui varie, ce n'est pas l'état lui-même : c'est l'ensemble des transitions légales d'un état vers un autre. Le réflexe : faire de l'état un ensemble fini, et des transitions une déclaration explicite — pour qu'une transition illégale soit impossible à exprimer, pas seulement déconseillée.status is a string, and any method assigns it any value. The order has a real lifecycle, though: you don't ship a cancelled order, you don't refund what was never paid. These are rules — but they live nowhere in one place. They're scattered as partial guards, assumed more than enforced. What varies isn't the state itself: it's the set of legal transitions from one state to another. The reflex: make the state a finite set, and the transitions an explicit declaration — so that an illegal transition is impossible to express, not merely discouraged.
Le code naïf — l'état qu'on mute de partoutThe naive code — state mutated from everywhere
# l'état est un champ libre — n'importe quel code peut y écrire n'importe quoiclass Order:
status: str = "pending"# une simple chaîne : aucune contraintedef ship(self): self.status = "shipped"# aucune vérification d'où l'on vientdef cancel(self): self.status = "cancelled"# … on peut annuler une commande expédiéedef refund(self): self.status = "refunded"# … rembourser sans avoir payé# rien n'empêche : ship() puis cancel() puis ship() à nouveau.# « une commande expédiée ne s'annule pas » est une RÈGLE — mais elle n'est# écrite nulle part. Les transitions illégales ne sont pas impossibles :# elles sont juste « pas censées arriver ». Et un jour, elles arrivent.
Le champ libre vs la machineThe free field vs the machine
status: strstatus: str
n'importe quelle valeur · muté partoutany value · mutated everywhere
⇄
ouor
états finis + transitionsfinite states + transitions
l'illégal · impossible à exprimerthe illegal · impossible to express
Un champ libre dit ce que l'état est maintenant, jamais d'où il peut venir ni où il peut aller. Tant que la transition n'est pas modélisée, « expédié → annulé » est aussi facile à écrire que « payé → expédié » : le compilateur, les tests, la relecture ne voient aucune différence — jusqu'à la production.A free field says what the state is now, never where it can come from or go. As long as the transition isn't modeled, 'shipped → cancelled' is as easy to write as 'paid → shipped': the compiler, the tests, the review see no difference — until production.
Le réflexe du volumeThe volume's reflex
« Quel détail je veux pouvoir changer sans toucher au reste ? » Ici le détail, c'est la table des transitions : qui mène à quoi. Tant qu'elle est diffuse en gardes éparses, la changer veut dire relire chaque mutation. La machine à états la rassemble en un artefact unique — l'ensemble des états, l'ensemble des transitions permises — et fait du passage d'un état à l'autre la seule porte d'entrée. L'illégal n'est plus refusé par un if qu'on peut oublier : il est absent de la table, donc irreprésentable.'Which detail do I want to change without touching the rest?' Here the detail is the transition table: what leads to what. As long as it's diffuse in scattered guards, changing it means rereading every mutation. The state machine gathers it into a single artifact — the set of states, the set of permitted transitions — and makes moving between states the only door in. The illegal is no longer refused by an if you can forget: it's absent from the table, hence unrepresentable.
« State » sort enfin de la parenthèse.'State' finally leaves the parenthesis.
Strategy (3.1) ne nommait State qu'en voisin, en passant. Il méritait sa propre marche, parce que son réflexe — « rendre les états illégaux impossibles » — est l'un des plus transférables de toute la série. C'est aussi un pont assumé vers la suite : un log Event Sourcing (Vol 5 #3) n'est que la suite des transitions acceptées, et une Saga (Vol 7) n'est qu'une machine à états distribuée à travers plusieurs services. Le même réflexe, à trois échelles : l'objet, l'agrégat, le système.Strategy (3.1) named State only as a neighbor, in passing. It deserved its own step, because its reflex — 'make illegal states impossible' — is one of the most transferable in the whole series. It's also a deliberate bridge to what follows: an Event Sourcing log (Vol 5 #3) is just the sequence of accepted transitions, and a Saga (Vol 7) is just a state machine distributed across several services. The same reflex, at three scales: the object, the aggregate, the system.
№ 02La douleurThe pain4 min
L'illégal arrive en prod, et personne ne sait dire ce qui peut suivre.The illegal reaches prod, and no one can say what may come next.
Laisser l'état dans un champ libre a trois coûts qui se renforcent. Les transitions illégales arrivent : rien n'empêche d'annuler une commande expédiée, alors un jour un appel mal ordonné le fait, et la base contient un état que le métier jurait impossible. Les règles de transition se dispersent : « on ne rembourse que ce qui a été payé » vit en gardes éparses, recopiées dans chaque méthode, jamais réunies. Et on ne peut pas répondre à « que peut-il se passer ensuite ? » : la seule façon de connaître les suites légales d'un état est de relire tout le code qui mute status — il n'existe nulle part une carte du cycle de vie.Leaving state in a free field has three costs that reinforce each other. Illegal transitions happen: nothing stops cancelling a shipped order, so one day a mis-ordered call does it, and the database holds a state the domain swore impossible. Transition rules scatter: 'refund only what was paid' lives in scattered guards, recopied in each method, never gathered. And you can't answer 'what may happen next?': the only way to know an state's legal successors is to reread every line that mutates status — nowhere is there a map of the lifecycle.
№ 1 · L'illégal devient réelThe illegal becomes real
« Une commande expédiée ne s'annule pas » n'est écrit nulle part comme une contrainte. Un jour, un retry, un webhook en double ou un appel mal ordonné assigne status = 'cancelled' sur une commande déjà partie. La base contient désormais un état que tout le métier jurait impossible — et plus rien ne sait le défaire.'A shipped order isn't cancellable' is written nowhere as a constraint. One day a retry, a duplicate webhook or a mis-ordered call sets status = 'cancelled' on an order already gone. The database now holds a state the whole domain swore impossible — and nothing knows how to undo it.
№ 2 · Les règles se dispersentThe rules scatter
« On ne rembourse que ce qui a été payé », « on n'expédie pas deux fois » : chaque règle de transition vit en garde partielle, dans la méthode qui y pense, copiée dans le service, oubliée dans le script d'import. La table des transitions n'existe pas ; elle est éparpillée, contradictoire, impossible à auditer d'un regard.'Refund only what was paid', 'don't ship twice': each transition rule lives as a partial guard, in the method that remembers it, copied in the service, forgotten in the import script. The transition table doesn't exist; it's scattered, contradictory, impossible to audit at a glance.
№ 3 · « Et après ? » sans réponse'What's next?' has no answer
Demande à un champ libre quelles suites sont légales depuis « payé » : il n'en sait rien. La seule façon de le savoir est de relire chaque endroit qui mute status. Il n'y a pas de carte du cycle de vie — donc pas de garde-fou pour le compilateur, pas de diagramme pour l'humain, pas de point unique à faire évoluer.Ask a free field which successors are legal from 'paid': it has no idea. The only way to know is to reread every place that mutates status. There is no map of the lifecycle — so no guardrail for the compiler, no diagram for the human, no single point to evolve.
Quatre endroits qui mutent status — aucun ne connaît les trois autresFour places that mutate status — none knows the other three
order.status = "shipped"# 💥 dans le service d'expédition
order.status = "cancelled"# 💥 dans le handler d'annulation — même si expédié
order.status = req.body["status"] # 💥 dans un endpoint d'admin : N'IMPORTE QUELLE chaîne
order.status = "refunded"# 💥 dans le webhook de paiement — sans vérifier "paid"# la règle « expédié ne s'annule pas » : nulle part. Quatre écritures, zéro carte.
Le diagnostic.The diagnosis.
Le détecteur : « existe-t-il des couples d'états entre lesquels un passage ne devrait jamais arriver — et puis-je quand même l'écrire en une ligne ? ». Si oui, l'état est un champ libre déguisé en cycle de vie. Le réflexe : énumérer les états finis, déclarer en un seul endroit les transitions permises, et faire de cette table la seule porte vers un changement d'état. Une transition absente de la table devient impossible à exprimer — pas refusée par un if qu'on peut oublier, mais irreprésentable. La règle se lit d'un regard, l'illégal ne compile plus, et « que peut-il se passer ensuite ? » a enfin une réponse : la table le dit.The detector: 'are there pairs of states a transition should never occur between — and can I still write it in one line?'. If yes, the state is a free field disguised as a lifecycle. The reflex: enumerate the finite states, declare the permitted transitions in one place, and make that table the only door to a state change. A transition absent from the table becomes impossible to express — not refused by a forgettable if, but unrepresentable. The rule reads at a glance, the illegal no longer compiles, and 'what may happen next?' finally has an answer: the table says so.
№ 03Les transitionsThe transitions5 min
On ferme l'ensemble des états, puis on ne câble que les passages légaux.Close the set of states, then wire only the legal passages.
La machine à états transforme un champ libre en cycle de vie déclaré. L'animation part du champ où toutes les flèches existent — y compris les interdites —, ferme l'ensemble en états finis nommés, câble en un seul endroit les transitions permises, puis montre l'essentiel : une transition absente de la table n'est pas refusée, elle est irreprésentable. À chaque marche, ce qui était une règle dans une tête devient une structure que le code, les tests et le diagramme partagent.A state machine turns a free field into a declared lifecycle. The animation starts from the field where every arrow exists — including the forbidden ones —, closes the set into named finite states, wires the permitted transitions in one place, then shows the essential: a transition absent from the table isn't refused, it's unrepresentable. At each step, what was a rule in someone's head becomes a structure the code, the tests and the diagram share.
Animation — du champ libre à la machine qui gardeAnimation — from the free field to the guarding machine
Départ — un champ libre : tout vers toutStart — a free field: anything to anything · 1/4
pendingpending
paidpaid
shippedshipped
cancelledcancelled
refundedrefunded
⇄ ⇄ ⇄ ⇄ ⇄
tout vers tout · y compris les flèches interditesanything to anything · including the forbidden arrows
status est une chaîne. Depuis n'importe quel état on peut écrire n'importe quel autre : expédié → en attente, remboursé → expédié. Toutes les flèches existent, y compris celles que le métier interdit. Rien ne les distingue.status is a string. From any state you can write any other: shipped → pending, refunded → shipped. Every arrow exists, including the ones the domain forbids. Nothing tells them apart.
La table EST le métier — 3 lignes à retrouverThe table IS the domain — 3 lines to recall
# les transitions permises, déclarées en UN endroit — c'est toute la règle
TRANSITIONS = {} # "shipped" n'a aucune sortie : il est terminaldef apply(state, event): # la SEULE porte vers un changement d'état
key = (state, event) raise IllegalTransition(state, event) # rebondit à l'entrée, pas en base
3 ligne(s) à retrouver — écris-la de tête avant de cliquer3 line(s) to recall — write it in your head before clicking
Le motif transférable.The transferable pattern.
« Un ensemble fini d'états, et une fonction (état, événement) → état qui n'admet que les passages déclarés. » Selon le langage, on l'exprime en table, en pattern matching exhaustif, ou en type-state où chaque état est un type distinct — et là, l'illégal ne compile littéralement pas. C'est le même réflexe à toutes les échelles : un workflow, un protocole réseau (TCP est une machine à états), un parseur, le cycle de vie d'un composant UI. Partout où « certains enchaînements ne doivent jamais arriver », la machine à états les rend impossibles plutôt que déconseillés.'A finite set of states, and a function (state, event) → state that admits only the declared passages.' Depending on the language, you express it as a table, exhaustive pattern matching, or type-state where each state is a distinct type — and there, the illegal literally doesn't compile. It's the same reflex at every scale: a workflow, a network protocol (TCP is a state machine), a parser, a UI component's lifecycle. Anywhere 'certain sequences must never happen', the state machine makes them impossible rather than discouraged.
Modèle mentalMental model
L'imageThe image
La grille d'une boîte de vitesses manuelle : les créneaux physiques n'autorisent que les passages prévus. Il n'y a pas de fente pour aller de la première à la marche arrière — la vitesse illégale n'est pas « interdite », elle est absente de la grille, donc impossible à sélectionner.A manual gearbox's gate: the physical slots allow only the intended shifts. There's no slot to go from first to reverse — the illegal gear isn't 'forbidden', it's absent from the gate, hence impossible to select.
La règleThe rule
Un ensemble fini d'états, et seules les transitions déclarées existent : l'illégal n'est pas refusé par une garde, il est absent de la table.A finite set of states, and only the declared transitions exist: the illegal isn't refused by a guard, it's absent from the table.
Où l'analogie casseWhere the analogy breaks
La grille de vitesses est physique : tu ne peux pas forcer une vitesse absente. En code, la « grille » (apply()) n'est étanche que si elle est la seule porte : laisse le champ d'état public, et n'importe qui passe la main par-dessus — la machine décorative du chapitre 4. L'analogie mécanique masque que, dans le logiciel, la contrainte ne vaut que ce que vaut son verrou. Seul le type-state — où l'état illégal ne compile pas — rend à la grille son impossibilité physique.The gearbox gate is physical: you can't force a gear that isn't there. In code, the 'gate' (apply()) is only tight if it's the sole door: leave the state field public, and anyone reaches around it — chapter 4's decorative machine. The mechanical analogy hides that, in software, the constraint is only worth its lock. Only type-state — where the illegal state doesn't compile — restores the gate's physical impossibility.
№ 04Le piègeThe trap4 min
Une machine qu'on peut contourner ne garde rien. La porte dérobée.A machine you can bypass guards nothing. The back door.
Le piège n'est pas la machine — c'est la porte qu'on laisse ouverte à côté. La dérive principale : une machine à états « décorative ». On déclare des transitions, on écrit apply(), mais l'état reste public et la moitié du code continue d'assigner order.state = '…' directement, « parce que c'était plus simple ». La garde existe et ne sert à rien : l'illégal reste atteignable par la fenêtre. L'excès inverse guette aussi : sur-modéliser un statechart à douze états pour un booléen, payer la cérémonie d'une machine là où un drapeau suffisait. Bascule le toggle : cherche l'écriture qui contourne l'unique porte.The trap isn't the machine — it's the door you leave open beside it. The main drift: a 'decorative' state machine. You declare transitions, you write apply(), but the state stays public and half the code keeps assigning order.state = '…' directly, 'because it was simpler'. The guard exists and is useless: the illegal stays reachable through the window. The opposite excess lurks too: over-modeling a twelve-state statechart for a boolean, paying a machine's ceremony where a flag sufficed. Flip the toggle: hunt the write that bypasses the only door.
# la machine est la SEULE porte : aucun setter public, aucune mutation directeclass Order:
def __init__(self):
self._state = "pending"# privé : personne n'écrit _state de l'extérieurdef apply(self, event): # l'unique entrée
self._state = transition(self._state, event) # lève si la flèche n'existe pas# ailleurs dans le code, on ne peut QUE :
order.apply("pay"); order.apply("ship")
# order._state = "refunded" ← ne se fait pas : l'état illégal est inatteignable par erreur.# la table des transitions EST la règle métier, en un seul endroit auditable.
Pourquoi c'est justeWhy it's right
L'état est privé, et apply() est la seule entrée : il n'existe aucun moyen, depuis le reste du code, de poser un état sans passer par la table des transitions. Un état illégal n'est donc pas « refusé » — il est inatteignable par erreur. La règle métier (qui mène à quoi) vit en un seul endroit auditable, et toute évolution du cycle de vie se fait là, pas dans vingt mutations dispersées. La machine ne décrit pas l'état après coup : elle est le seul chemin pour y arriver. C'est ce qui fait la différence entre une garde qu'on peut oublier et une contrainte qu'on ne peut pas violer.The state is private, and apply() is the only entry: there is no way, from the rest of the code, to set a state without going through the transition table. So an illegal state isn't 'refused' — it's unreachable by mistake. The business rule (what leads to what) lives in one auditable place, and every lifecycle change happens there, not in twenty scattered mutations. The machine doesn't describe the state after the fact: it is the only path to reach it. That's the difference between a guard you can forget and a constraint you can't violate.
La règle qui tranche.The rule that settles it.
Modélise une machine à états seulement quand il y a un vrai cycle de vie fini — plusieurs états, des transitions que le métier interdit — ET que la machine est la seule porte vers un changement d'état. Le test : « un bout de code peut-il atteindre un état illégal par erreur ? ». Si oui (état public, setter, réinjection d'un champ libre), tu as une décoration, pas une garde. La forme la plus forte est le type-state : chaque état est un type distinct, et l'illégal ne compile pas — l'« état illégal » n'a tout simplement pas de représentation. La machine ne vaut que par la porte qu'elle ferme.Model a state machine only when there's a real finite lifecycle — several states, transitions the domain forbids — AND the machine is the only door to a state change. The test: 'can a piece of code reach an illegal state by mistake?'. If yes (public state, setter, reinjecting a free field), you have decoration, not a guard. The strongest form is type-state: each state is a distinct type, and the illegal doesn't compile — the 'illegal state' simply has no representation. A machine is only worth the door it closes.
Le même réflexe, à trois échelles.The same reflex, at three scales.
« Rendre les états illégaux impossibles » ne s'arrête pas à l'objet. Un log Event Sourcing (Vol 5 #3) n'est que la suite des transitions acceptées par une machine : l'historique est la machine déroulée dans le temps. Une Saga (Vol 7) est cette même machine distribuée à travers plusieurs services, où chaque étape compense la précédente à l'échec. L'objet, l'agrégat, le système : trois échelles, un seul réflexe — n'autoriser que les passages déclarés, et faire de l'illégal une chose qu'on ne peut pas écrire.'Make illegal states impossible' doesn't stop at the object. An Event Sourcing log (Vol 5 #3) is just the sequence of transitions a machine accepted: history is the machine unrolled in time. A Saga (Vol 7) is that same machine distributed across services, where each step compensates the previous on failure. The object, the aggregate, the system: three scales, one reflex — admit only declared passages, and make the illegal something you cannot write.
№ 05Bilan & éditoWrap-up & editorial2 min
Un champ qu'on espère correct, ou une table où l'illégal n'existe pas.A field you hope is correct, or a table where the illegal doesn't exist.
Champ libre vs machine à états, côte à côteFree field vs state machine, side by side
champ librefree field
machine à étatsstate machine
L'illégalThe illegal
« pas censé arriver »'not supposed to happen'
impossible à exprimerimpossible to express
La règleThe rule
éparse, en gardes recopiéesscattered, in recopied guards
Quand ne pas faire de machineWhen not to build a machine
La machine a un loyer : une table à tenir, une seule porte à défendre, une indirection de plus. On reste avec un simple champ — et c'est correct — quand :The machine has rent: a table to maintain, a single door to defend, one more indirection. Stay with a plain field — and that's fine — when:
il n'y a que deux états sans transition coûteuse : un drapeau actif/inactif, lu/non-lu. Un booléen dit tout ; une machine ici n'est que de la cérémonie (YAGNI, Vol 0 #3) — la sur-modélisation du chapitre 4.there are only two states with no costly transition: an active/inactive, read/unread flag. A boolean says it all; a machine here is mere ceremony (YAGNI, Vol 0 #3) — chapter 4's over-modeling.
aucune transition n'est réellement interdite : si tout état peut légitimement suivre tout autre, il n'y a pas de règle à protéger, donc rien que la machine rende impossible. Elle n'a de valeur que par les flèches qu'elle absente.no transition is truly forbidden: if any state can legitimately follow any other, there's no rule to protect, so nothing the machine makes impossible. It's only worth the arrows it leaves out.
le cycle de vie est ouvert ou inconnu : si les états apparaissent et disparaissent au gré du produit, figer une table trop tôt, c'est se battre contre le changement à chaque release. Attends que la forme se stabilise avant de la sceller.the lifecycle is open or unknown: if states come and go with the product, freezing a table too early means fighting change at every release. Wait for the shape to stabilize before sealing it.
Le réflexe n'est donc pas « modélise toujours une machine à états, c'est plus rigoureux » : c'est « existe-t-il un cycle de vie fini avec des passages interdits ? Alors une table, et une seule porte. Sinon, un champ honnête vaut mieux qu'une machine qu'on contourne. »So the reflex isn't 'always model a state machine, it's more rigorous': it's 'is there a finite lifecycle with forbidden passages? Then a table, and a single door. Otherwise, an honest field beats a machine you bypass.'
Mot de l'éditeurFrom the editor
Ce numéro tire « State » de la parenthèse où le #1 l'avait laissé, parce que son réflexe — rendre les états illégaux impossibles — est l'un des plus transférables de toute la série. La leçon n'est pas « apprends le pattern State du GoF » : c'est « ne laisse pas une règle de transition vivre à l'état de vœu pieux ». Un champ libre espère que le code se tiendra bien ; une machine à états le rend incapable de mal se tenir. La nuance qui sauve : la machine ne vaut que si elle est la seule porte — une garde qu'on peut contourner ne garde rien.This issue pulls 'State' out of the parenthesis #1 left it in, because its reflex — making illegal states impossible — is one of the most transferable in the whole series. The lesson isn't 'learn the GoF State pattern': it's 'don't let a transition rule live as a pious wish'. A free field hopes the code behaves; a state machine makes it unable to misbehave. The saving nuance: the machine is only worth it if it's the only door — a guard you can bypass guards nothing.
Avec lui se referme le Vol 3, les patterns d'objets dont le réflexe survit : Strategy injectait un comportement à la place d'un if/else, et State n'en est que le cas particulier où le comportement, c'est l'état lui-même et ses passages. On boucle aussi le fil rouge fonctionnel tendu depuis le début : le pattern State du GoF — une classe par état, du polymorphisme partout — s'effondre, en programmation fonctionnelle, en une seule fonction pure (état, événement) → état. Tout l'échafaudage objet n'était qu'une manière de transporter cette fonction. La suite quitte l'objet pour le système : le Vol 4 ouvre la communication entre composants, où la première question n'est plus quelle transition est légale, mais « ai-je besoin d'une réponse maintenant, ou seulement que ce soit fait un jour ? ».With it Vol 3 closes, the object patterns whose reflex survives: Strategy injected a behavior in place of an if/else, and State is just the special case where the behavior is the state itself and its passages. We also close the functional thread strung from the start: the GoF State pattern — one class per state, polymorphism everywhere — collapses, in functional programming, into a single pure function (state, event) → state. All the object scaffolding was just a way to carry that function. What follows leaves the object for the system: Vol 4 opens communication between components, where the first question is no longer which transition is legal, but 'do I need an answer now, or only that it gets done someday?'.
Les voisins que ce numéro débloqueThe neighbors this issue unlocks
State (GoF) — la version objet : une classe par état, chacune sachant vers quels états elle peut transiter. Utile quand chaque état porte beaucoup de comportement propre ; mais souvent plus lourd qu'une table, et c'est lui qui s'effondre en (état, événement) → état dès qu'on raisonne en fonctions.the object version: one class per state, each knowing which states it can transition to. Useful when each state carries a lot of its own behavior; but often heavier than a table, and it's the one that collapses into (state, event) → state once you reason in functions.
Statecharts — la machine à états poussée : états imbriqués, états parallèles, gardes et actions sur les transitions. Indispensable pour une UI ou un protocole riche ; le danger est exactement celui du chapitre 4 — sur-modéliser, et payer un statechart là où une table plate suffisait.the state machine scaled up: nested states, parallel states, guards and actions on transitions. Indispensable for a rich UI or protocol; the danger is exactly chapter 4's — over-model, and pay for a statechart where a flat table sufficed.
Type-state — la forme la plus forte : chaque état est un type distinct, et les transitions illégales ne compilent pas. L'« état illégal » n'a plus de représentation du tout — c'est « rendre les états illégaux irreprésentables » porté jusqu'au système de types. La fin du voyage commencé au chapitre 1.the strongest form: each state is a distinct type, and illegal transitions don't compile. The 'illegal state' has no representation at all — it's 'make illegal states unrepresentable' carried into the type system. The end of the journey begun in chapter 1.
Le kata — écris-le, ne le relis pasThe kata — write it, don't reread it
Tu as vu le champ libre se resserrer en table, puis l'illégal devenir irreprésentable dans l'animation du chapitre 3. Reste le geste qui ancre : l'écrire. Copie ce fichier de tests dans ton langage habituel — une table, un apply() qui garde, un état privé — et fais-les passer au vert sans revenir au numéro. Le dernier test est le sournois : « shipped » est terminal, et une machine juste se juge autant à ses flèches absentes qu'à ses flèches présentes. (Le disjoncteur du Vol 7 — fermé, ouvert, demi-ouvert — n'est que cette même machine, à trois états.)You watched the free field tighten into a table, then the illegal become unrepresentable in chapter 3's animation. What's left is the gesture that anchors it: writing it. Copy this test file into your usual language — a table, an apply() that guards, a private state — and turn them green without coming back to the issue. The last test is the sneaky one: 'shipped' is terminal, and a correct machine is judged as much by its absent arrows as by its present ones. (Vol 7's circuit breaker — closed, open, half-open — is just this same machine, with three states.)
test_order_state.py
# test_order_state.py — la spec EST le harnais : fais-le passer au vert.# un ensemble fini d'états, une table de transitions permises, et apply() pour# seule porte. l'illégal n'est pas refusé : il est absent de la table.def test_le_chemin_nominal_traverse_les_etats():
o = Order() # démarre "pending"
o.apply("pay"); o.apply("ship")
assert o.state == "shipped"def test_une_transition_illegale_est_rejetee():
o = Order() # "pending"
with raises(IllegalTransition):
o.apply("ship") # on ne livre pas une commande non payéedef test_l_illegale_ne_corrompt_pas_l_etat():
o = Order()
try: o.apply("ship") # rejetée…
except IllegalTransition: pass
assert o.state == "pending"# …et l'état n'a pas bougé d'un poucedef test_un_evenement_inconnu_echoue_pareil():
o = Order()
with raises(IllegalTransition):
o.apply("teleport") # pas dans la table = illégal, pointdef test_les_deux_sorties_partent_des_bons_etats():
assert Order().apply("cancel").state == "cancelled"# cancel depuis pending : ok
paid = Order(); paid.apply("pay")
assert paid.apply("refund").state == "refunded"# refund depuis paid : okdef test_l_etat_terminal_n_a_aucune_sortie():
# LE test du pattern : "shipped" est terminal — AUCUN événement ne l'en sort.# une machine juste se juge autant à ses flèches absentes qu'à ses flèches présentes.
o = Order(); o.apply("pay"); o.apply("ship") # → shippedfor ev in ["pay", "ship", "cancel", "refund"]:
with raises(IllegalTransition): o.apply(ev)
assert o.state == "shipped"
Chez toi, cette semaine.In your codebase, this week.
Compte les portes de ton état le plus sensible — commande, paiement, abonnement : grep -rnE "\.(status|state)\s*=[^=]" src/ liste chaque endroit qui ÉCRIT le champ. Un seul (dans une méthode apply/transition) = une vraie machine. Trois, cinq, douze, dispersés dans des handlers ? C'est un champ libre — ou une machine décorative qu'on contourne par la fenêtre. La question du chapitre 4 : un bout de code peut-il atteindre un état illégal par erreur ?Count the doors to your most sensitive state — order, payment, subscription: grep -rnE "\.(status|state)\s*=[^=]" src/ lists every place that WRITES the field. One (inside an apply/transition method) = a real machine. Three, five, twelve, scattered across handlers? It's a free field — or a decorative machine bypassed through the window. Chapter 4's question: can a piece of code reach an illegal state by mistake?