Polar Code 🎭

Command Palette

Search for a command to run...

29
Pièce N°29

CHAPITRE 28 – MEMENTO

Le suspect avait craqué, avait tout avoué. Mais un doute subsistait. Et si on avait trop forcé ? Il fallait pouvoir revenir en arrière, à l’état de l’interrogatoire avant la pression. Le magnétophone était là, bande toujours enregistrée. On pouvait rembobiner, réécouter le moment exact. C’était un Memento. Une capsule temporelle. Dans le code, c’était la capacité de capturer et de restaurer l’état interne d’un objet sans violer son encapsulation.

Sauvegarde d’état

Le pattern Memento permet de sauvegarder l’état d’un objet (l’Originator) dans un objet Memento, de manière à pouvoir restaurer l’Originator à cet état plus tard. C’est la base des fonctionnalités d’annulation (undo), de retour à un point de sauvegarde, ou de snapshot.

Le défi est de le faire sans exposer les détails internes de l’Originator. Le Memento doit être un conteneur opaque : l’Originateur peut y écrire et y lire, mais les autres objets (le Caretaker – le gardien) ne doivent pas pouvoir en altérer le contenu.

Implémentation avec encapsulation stricte en TypeScript

// --- ORIGINATOR : L'objet dont on veut sauvegarder l'état ---
class DossierEnquete {
    private suspect: string;
    private preuves: string[];
    private hypothese: string;
    private confidence: number; // 0-100

    constructor(suspect: string) {
        this.suspect = suspect;
        this.preuves = [];
        this.hypothese = '';
        this.confidence = 50;
        console.log(`📁 Nouveau dossier ouvert pour ${suspect}. Confiance initiale: ${this.confidence}%`);
    }

    // Méthodes qui modifient l'état
    ajouterPreuve(preuve: string): void {
        this.preuves.push(preuve);
        this.confidence = Math.min(100, this.confidence + 10);
        this.mettreAJourHypothese();
        console.log(`   [+] Preuve ajoutée: "${preuve}". Confiance: ${this.confidence}%`);
    }

    contredirePreuve(): void {
        if (this.preuves.length > 0) {
            const preuveContredite = this.preuves.pop();
            this.confidence = Math.max(0, this.confidence - 20);
            this.mettreAJourHypothese();
            console.log(`   [-] Preuve contredite: "${preuveContredite}". Confiance: ${this.confidence}%`);
        }
    }

    changerHypothese(nouvelleHypothese: string): void {
        this.hypothese = nouvelleHypothese;
        console.log(`   🔄 Hypothèse changée: "${this.hypothese}"`);
    }

    private mettreAJourHypothese(): void {
        if (this.confidence > 75) {
            this.hypothese = `${this.suspect} est probablement coupable`;
        } else if (this.confidence < 25) {
            this.hypothese = `${this.suspect} est probablement innocent`;
        } else {
            this.hypothese = 'Affaire incertaine';
        }
    }

    afficherEtat(): void {
        console.log(`\n📋 ÉTAT ACTUEL DU DOSSIER:`);
        console.log(`   Suspect: ${this.suspect}`);
        console.log(`   Preuves: [${this.preuves.join(', ')}]`);
        console.log(`   Hypothèse: ${this.hypothese}`);
        console.log(`   Confiance: ${this.confidence}%\n`);
    }

    // --- PARTIE MEMENTO ---
    // Sauvegarde l'état actuel dans un Memento
    sauvegarder(): Memento {
        console.log(`💾 Sauvegarde de l'état (confiance: ${this.confidence}%)...`);
        // Le Memento est créé avec l'état interne. Seul l'Originator y a accès.
        return new MementoImpl({
            suspect: this.suspect,
            preuves: [...this.preuves], // Copie du tableau
            hypothese: this.hypothese,
            confidence: this.confidence
        });
    }

    // Restaure l'état à partir d'un Memento
    restaurer(memento: Memento): void {
        // On cast en MementoImpl car on sait que c'est notre implémentation.
        // En pratique, on pourrait vérifier le type.
        const etat = (memento as MementoImpl).getState();
        this.suspect = etat.suspect;
        this.preuves = [...etat.preuves]; // Nouvelle copie
        this.hypothese = etat.hypothese;
        this.confidence = etat.confidence;
        console.log(`↩️  RESTAURATION à l'état sauvegardé (confiance: ${this.confidence}%)`);
    }

    // Classe Memento interne et privée (ou dans un module séparé)
    // Elle est exportée pour être utilisée par le Caretaker, mais son contenu est caché.
}

// --- INTERFACE MEMENTO (publique mais vide - marqueur) ---
interface Memento {
    // Vide intentionnellement. Le Caretaker ne peut pas manipuler le contenu.
}

// --- IMPLÉMENTATION CONCRÈTE DU MEMENTO (privée à l'Originator) ---
// On la met dans le même scope, mais on pourrait utiliser un module/namespace.
class MementoImpl implements Memento {
    // L'état sauvegardé. Privé, inaccessible de l'extérieur.
    constructor(private state: {
        suspect: string;
        preuves: string[];
        hypothese: string;
        confidence: number;
    }) {}

    // Méthode INTERNE, accessible seulement par l'Originator (ami en C++, package en Java).
    // En TS, on fait confiance au scope. On ne l'exporte pas.
    getState() {
        return this.state;
    }
}

// --- CARETAKER : Le gardien des Mementos ---
class HistoriqueEnquete {
    private mementos: Memento[] = [];
    private pointer: number = -1; // Pour undo/redo

    sauvegarder(originator: DossierEnquete): void {
        // Tronquer l'historique après le pointer actuel (si on a fait undo)
        this.mementos = this.mementos.slice(0, this.pointer + 1);
        const memento = originator.sauvegarder();
        this.mementos.push(memento);
        this.pointer++;
        console.log(`   📍 Historique: sauvegarde #${this.pointer + 1}`);
    }

    undo(originator: DossierEnquete): boolean {
        if (this.pointer <= 0) {
            console.log(`   ⚠️  Impossible de undo davantage.`);
            return false;
        }
        this.pointer--;
        const memento = this.mementos[this.pointer];
        originator.restaurer(memento);
        console.log(`   ↩️  UNDO vers la sauvegarde #${this.pointer + 1}`);
        return true;
    }

    redo(originator: DossierEnquete): boolean {
        if (this.pointer >= this.mementos.length - 1) {
            console.log(`   ⚠️  Impossible de redo davantage.`);
            return false;
        }
        this.pointer++;
        const memento = this.mementos[this.pointer];
        originator.restaurer(memento);
        console.log(`   ↪️  REDO vers la sauvegarde #${this.pointer + 1}`);
        return true;
    }

    afficherHistorique(): void {
        console.log(`\n📜 HISTORIQUE (${this.mementos.length} sauvegardes, pointer: ${this.pointer})`);
    }
}

// --- SCÉNARIO : ENQUÊTE AVEC UNDO/REDO ---
console.log("=== SYSTÈME D'ENQUÊTE AVEC MEMENTO (Undo/Redo) ===\n");

const enquete = new DossierEnquete("M. X");
const historique = new HistoriqueEnquete();

// Point de départ
historique.sauvegarder(enquete);
enquete.afficherEtat();

// Ajout de preuves
enquete.ajouterPreuve("Empreintes sur l'arme");
historique.sauvegarder(enquete);
enquete.ajouterPreuve("Témoin oculaire");
historique.sauvegarder(enquete);
enquete.changerHypothese("Coupable avec préméditation");
historique.sauvegarder(enquete);
enquete.afficherEtat();

// Oups, la preuve est contredite
console.log("\n--- CONTRE-ENQUÊTE ---");
enquete.contredirePreuve(); // Témoin rétracté
historique.sauvegarder(enquete);
enquete.afficherEtat();

// UNDO : On revient avant la rétractation
console.log("\n--- UNDO ---");
historique.undo(enquete);
enquete.afficherEtat();

// UNDO encore : On revient avant le changement d'hypothèse
historique.undo(enquete);
enquete.afficherEtat();

// REDO : On avance à nouveau
console.log("\n--- REDO ---");
historique.redo(enquete);
enquete.afficherEtat();

// Nouvelle piste
console.log("\n--- NOUVELLE PISTE ---");
enquete.ajouterPreuve("Alibi vérifié");
// On oublie de sauvegarder ! L'historique reste au dernier point sauvegardé.

// UNDO après oubli de sauvegarde : on revient au dernier point connu
console.log("\n--- UNDO (après oubli) ---");
historique.undo(enquete);
enquete.afficherEtat(); // L'alibi n'est pas là, car on est revenu en arrière.

historique.afficherHistorique();

Encapsulation

Le défi du Memento est de préserver l’encapsulation. L’état de l’Originator, souvent composé de membres privés, doit être copié dans le Memento sans être exposé. Solutions :

  1. Classe interne (comme ici) : Le Memento est une classe privée dans le même module/namespace que l’Originator. Seul l’Originator peut instancier et lire le Memento.
  2. Interface opaque : L’interface Memento est publique mais vide. L’implémentation concrète, avec les données, est dans un package/fichier non exporté.
  3. Sérialisation : L’Originator sérialise son état en une chaîne ou un objet simple, et le désérialise lors de la restauration. Le Memento ne contient que cette représentation opaque. C’est plus simple mais moins performant et nécessite une logique de sérialisation.

Coût mémoire

C’est la contrepartie. Chaque Memento est une copie complète (ou profonde) de l’état de l’Originator. Si l’état est volumineux (une image, un document complexe) et qu’on garde beaucoup de mementos, la mémoire explose.

Optimisations possibles :

  • Différentiel : Ne sauvegarder que les changements par rapport au précédent memento (comme un VCS).
  • Compression : Compresser les données dans le memento.
  • Éviction : Limiter le nombre de mementos gardés en mémoire (file circulaire, suppression des anciens).
  • Lazy copy : Utiliser des structures persistentes ou copy-on-write.

Cas d’usage

  • Undo/Redo dans les applications (éditeurs, logiciels de CAO).
  • Snapshots de configuration ou d’état de jeu.
  • Transactions : pouvoir rollback à un état précédent si une opération échoue.
  • Debugging : sauvegarder l’état avant une opération risquée.

Le magnétophone était arrêté. Le Memento était le gardien du temps logiciel. Il permettait de voyager en arrière, de corriger les erreurs, d’explorer des branches alternatives sans risque.

C’était l’avant-dernier pattern. Le dernier, Interpreter, serait le plus obscur, le plus niché : un pattern pour implémenter un langage simple. On l’utilise rarement, mais quand on en a besoin, il n’y a pas d’alternative.