Polar Code 🎭

Command Palette

Search for a command to run...

24
Pièce N°24

CHAPITRE 23 – STATE

Le suspect passait par des phases : déni, colère, négociation, dépression, acceptation. L’interrogatoire devait s’adapter. Si on utilisait la même approche à chaque phase, c’était l’échec. Dans le code, c’était pareil : un objet avec un enum etat et des méthodes pleines de switch (this.etat) { case 'DENI': ... }. La logique était éparpillée, difficile à suivre. Il fallait matérialiser chaque état en un objet distinct, avec son propre comportement. Il fallait un State.

États explicites

Le pattern State permet à un objet de modifier son comportement quand son état interne change. L’objet apparaîtra comme ayant changé de classe. L’astuce est de représenter chaque état possible par une classe concrète qui implémente une interface d’état commune. L’objet contexte (l’objet dont l’état change) délègue alors le comportement dépendant de l’état à l’objet état courant.

Cela rend les états explicites : chaque état est une classe, facile à identifier, à tester, à modifier.

Finite State Machine (Machine à états finis)

Le pattern State est souvent utilisé pour implémenter une Machine à États Finis (FSM), un modèle conceptuel où un système peut être dans un nombre fini d’états, et où les transitions entre états sont déclenchées par des événements.

Exemple : Distributeur de boissons

Un distributeur classique a des états : SansArgent, AvecArgent, Vendu, Epuisé. Son comportement (insererPiece(), ejecterPiece(), selectionnerBoisson(), delivrer()) change radicalement selon l’état.

// Interface État (State)
interface EtatDistributeur {
    insererPiece(): void;
    ejecterPiece(): void;
    selectionnerBoisson(): void;
    delivrer(): void;
}

// Contexte : Le Distributeur
class Distributeur {
    // Références à tous les états possibles (créés une fois)
    private etatSansArgent: EtatDistributeur;
    private etatAvecArgent: EtatDistributeur;
    private etatVendu: EtatDistributeur;
    private etatEpuise: EtatDistributeur;

    private etatCourant: EtatDistributeur;
    private nombreBoissons: number = 2; // Stock initial

    constructor() {
        // Création des états, en leur passant une référence au contexte (this)
        this.etatSansArgent = new EtatSansArgent(this);
        this.etatAvecArgent = new EtatAvecArgent(this);
        this.etatVendu = new EtatVendu(this);
        this.etatEpuise = new EtatEpuise(this);

        // État initial
        this.etatCourant = this.nombreBoissons > 0 ? this.etatSansArgent : this.etatEpuise;
        console.log(`🚀 Distributeur initialisé. Stock: ${this.nombreBoissons}. État: ${this.etatCourant.constructor.name}`);
    }

    // Méthodes pour changer d'état (appelées par les objets State)
    setEtat(etat: EtatDistributeur): void {
        this.etatCourant = etat;
        console.log(`   ➡️  Transition vers l'état: ${etat.constructor.name}`);
    }

    // Méthodes d'action qui délèguent à l'état courant
    insererPiece(): void {
        this.etatCourant.insererPiece();
    }
    ejecterPiece(): void {
        this.etatCourant.ejecterPiece();
    }
    selectionnerBoisson(): void {
        this.etatCourant.selectionnerBoisson();
    }
    delivrer(): void {
        this.etatCourant.delivrer();
    }

    // Méthodes utilitaires pour les États
    libererUneBoisson(): void {
        if (this.nombreBoissons > 0) {
            this.nombreBoissons--;
            console.log(`   🥤 Une boisson est délivrée. Stock restant: ${this.nombreBoissons}`);
        }
    }
    getStock(): number {
        return this.nombreBoissons;
    }
    recharger(nombre: number): void {
        this.nombreBoissons += nombre;
        console.log(`   📦 Rechargé de ${nombre} boissons. Nouveau stock: ${this.nombreBoissons}`);
        // Si on était en état Epuisé et qu'on recharge, on repasse à SansArgent
        if (this.etatCourant instanceof EtatEpuise && this.nombreBoissons > 0) {
            this.setEtat(this.etatSansArgent);
        }
    }

    // Accesseurs pour les états (utilisés par les classes d'état)
    getEtatSansArgent(): EtatDistributeur { return this.etatSansArgent; }
    getEtatAvecArgent(): EtatDistributeur { return this.etatAvecArgent; }
    getEtatVendu(): EtatDistributeur { return this.etatVendu; }
    getEtatEpuise(): EtatDistributeur { return this.etatEpuise; }
}

// États Concrets
class EtatSansArgent implements EtatDistributeur {
    constructor(private distributeur: Distributeur) {}

    insererPiece(): void {
        console.log("💰 Pièce acceptée.");
        this.distributeur.setEtat(this.distributeur.getEtatAvecArgent());
    }
    ejecterPiece(): void {
        console.log("❌ Aucune pièce à éjecter.");
    }
    selectionnerBoisson(): void {
        console.log("❌ Veuillez insérer une pièce d'abord.");
    }
    delivrer(): void {
        console.log("❌ Veuillez insérer une pièce et sélectionner une boisson.");
    }
}

class EtatAvecArgent implements EtatDistributeur {
    constructor(private distributeur: Distributeur) {}

    insererPiece(): void {
        console.log("❌ Vous avez déjà inséré une pièce.");
    }
    ejecterPiece(): void {
        console.log("🔄 Pièce rendue.");
        this.distributeur.setEtat(this.distributeur.getEtatSansArgent());
    }
    selectionnerBoisson(): void {
        console.log("✅ Boisson sélectionnée.");
        this.distributeur.setEtat(this.distributeur.getEtatVendu());
    }
    delivrer(): void {
        console.log("❌ Aucune boisson sélectionnée.");
    }
}

class EtatVendu implements EtatDistributeur {
    constructor(private distributeur: Distributeur) {}

    insererPiece(): void {
        console.log("⏳ Veuillez patienter, votre boisson arrive.");
    }
    ejecterPiece(): void {
        console.log("❌ Trop tard, la boisson a déjà été sélectionnée.");
    }
    selectionnerBoisson(): void {
        console.log("❌ Déjà en cours de délivrance.");
    }
    delivrer(): void {
        this.distributeur.libererUneBoisson();
        if (this.distributeur.getStock() > 0) {
            this.distributeur.setEtat(this.distributeur.getEtatSansArgent());
        } else {
            this.distributeur.setEtat(this.distributeur.getEtatEpuise());
        }
    }
}

class EtatEpuise implements EtatDistributeur {
    constructor(private distributeur: Distributeur) {}

    insererPiece(): void {
        console.log("❌ Plus de boissons disponibles. Pièce refusée.");
    }
    ejecterPiece(): void {
        console.log("❌ Aucune pièce à éjecter (stock épuisé).");
    }
    selectionnerBoisson(): void {
        console.log("❌ Plus de boissons disponibles.");
    }
    delivrer(): void {
        console.log("❌ Plus de boissons à délivrer.");
    }
}

// --- Scénario d'utilisation ---
console.log("=== DISTRIBUTEUR DE BOISSONS (State Pattern) ===\n");
const distributeur = new Distributeur();

console.log("\n--- Scénario normal ---");
distributeur.insererPiece();   // SansArgent -> AvecArgent
distributeur.selectionnerBoisson(); // AvecArgent -> Vendu
distributeur.delivrer();       // Vendu -> SansArgent (si stock > 0)

console.log("\n--- Tentative d'arnaque ---");
distributeur.insererPiece();
distributeur.ejecterPiece();   // AvecArgent -> SansArgent
distributeur.delivrer();       // Devrait échouer (SansArgent)

console.log("\n--- Épuisement du stock ---");
distributeur.insererPiece();
distributeur.selectionnerBoisson();
distributeur.delivrer();       // Dernière boisson. Vendu -> Epuisé

console.log("\n--- Comportement en état Epuisé ---");
distributeur.insererPiece();   // Refusé
distributeur.selectionnerBoisson(); // Refusé

console.log("\n--- Rechargement ---");
distributeur.recharger(3);     // Epuisé -> SansArgent

console.log("\n--- Nouvel achat après rechargement ---");
distributeur.insererPiece();
distributeur.selectionnerBoisson();
distributeur.delivrer();

Comparaison avec Strategy

C’est une confusion classique. Les deux patterns utilisent la composition pour déléguer un comportement à un objet externe. Mais l’intention est radicalement différente.

AspectStateStrategy
IntentionGérer les changements d’état interne d’un objet. Le comportement change automatiquement avec l’état.Définir une famille d’algorithmes interchangeables. L’algorithme est choisi délibérément par le client.
TransitionLes états connaissent souvent les autres états et peuvent initier des transitions (setEtat()).Les stratégies sont indépendantes, elles ne connaissent pas les autres stratégies. Aucune transition automatique.
Nombre d’instancesSouvent, une instance de chaque état concret est créée et réutilisée (comme dans le distributeur).On peut créer plusieurs instances d’une même stratégie, ou une nouvelle à chaque fois.
DéclenchementChangement réactif à des événements internes.Changement actif, décidé par le contexte ou un client externe.
AnalogieUne personne dont l’humeur (état) change son comportement. La tristesse mène à pleurer, la joie à rire.Un navigateur GPS qui propose différents algorithmes de routage (rapide, écologique, panoramique). Le conducteur choisit.

En résumé :

  • State : « Je me comporte différemment parce que je suis dans un état différent. »
  • Strategy : « Je me comporte différemment parce que j’ai choisi d’utiliser un algorithme différent pour cette tâche. »

Avantages du pattern State

  • Organisation claire : Chaque état est dans sa propre classe. Plus de switch monstrueux.
  • Principe Ouvert/Fermé : Ajouter un nouvel état = ajouter une nouvelle classe, sans modifier les états existants ni le contexte (sauf pour l’initialiser).
  • Transition explicite : Les transitions sont matérialisées par des appels à setEtat(), ce qui les rend faciles à tracer et à comprendre.
  • Partage d’états : Si les états n’ont pas d’état interne propre (ils sont stateless), on peut partager une seule instance (comme le Singleton d’un état).

Inconvénients

  • Surabondance de classes : Pour un système avec beaucoup d’états, on se retrouve avec beaucoup de petites classes.
  • Complexité : Le pattern peut être overkill si l’objet n’a que 2 ou 3 états et que la logique est simple.

Le suspect avait désormais des états bien définis, chacun avec son protocole. Le pattern State avait remplacé la logique conditionnelle spaghetti par une machine à états claire, modulaire et extensible.

C’était l’outil parfait pour modéliser tout ce qui a un cycle de vie, des workflows, des modes opératoires.

Le prochain pattern, Strategy, serait son cousin conceptuel mais avec une intention opposée : non pas réagir à un changement interne, mais fournir délibérément un algorithme interchangeable depuis l’extérieur.