Polar Code 🎭

Command Palette

Search for a command to run...

22
Pièce N°22

CHAPITRE 21 – OBSERVER

Le tableau d'affichage du commissariat. Quand un nouvel avis de recherche était épinglé, les inspecteurs dans la salle levaient la tête. Certains prenaient des notes, d'autres sortaient pour enquêter, d'autres l'ignoraient. Le tableau ne connaissait pas les inspecteurs. Il ne faisait que publier. Les inspecteurs s'abonnaient en le regardant. C'était le pattern Observer en action. Dans le code, c'était la même chose : un objet devait notifier d'autres objets de ses changements d'état, sans les connaître directement.

Notification et dépendances

Le problème est celui des dépendances un-à-plusieurs. Un objet (le Sujet ou Observable) change d'état. Plusieurs autres objets (les Observateurs) ont besoin d'être notifiés de ce changement pour mettre à jour leur propre état. Sans pattern, le Sujet doit maintenir une liste explicite de ses dépendants et les appeler directement, créant un couplage fort.

L'Observer inverse cette dépendance. Les Observateurs s'abonnent au Sujet. Le Sujet maintient une liste d'Observateurs et les notifie lorsqu'un événement se produit. Le Sujet ne dépend que d'une interface d'Observateur, pas des implémentations concrètes.

Implémentation : Système d'alerte météo

// Interface Observateur (Observer)
interface Observateur {
    actualiser(temperature: number, humidite: number, pression: number): void;
}

// Interface Sujet (Observable)
interface SujetMeteo {
    enregistrerObservateur(o: Observateur): void;
    supprimerObservateur(o: Observateur): void;
    notifierObservateurs(): void;
}

// Sujet Concret
class StationMeteo implements SujetMeteo {
    private observateurs: Observateur[] = [];
    private temperature: number = 0;
    private humidite: number = 0;
    private pression: number = 0;

    enregistrerObservateur(o: Observateur): void {
        this.observateurs.push(o);
        console.log(`👤 Observateur enregistré. Total: ${this.observateurs.length}`);
    }

    supprimerObservateur(o: Observateur): void {
        const index = this.observateurs.indexOf(o);
        if (index > -1) {
            this.observateurs.splice(index, 1);
            console.log(`👤 Observateur supprimé. Restants: ${this.observateurs.length}`);
        }
    }

    notifierObservateurs(): void {
        console.log(`📢 Notification de ${this.observateurs.length} observateur(s)...`);
        for (const observateur of this.observateurs) {
            observateur.actualiser(this.temperature, this.humidite, this.pression);
        }
    }

    // Méthode appelée quand les mesures changent (simulée)
    setMesures(temperature: number, humidite: number, pression: number): void {
        console.log(`\n🌡️  Mise à jour des mesures: ${temperature}°C, ${humidite}%, ${pression}hPa`);
        this.temperature = temperature;
        this.humidite = humidite;
        this.pression = pression;
        this.notifierObservateurs(); // Le point crucial : notification automatique
    }

    // Autres méthodes métier de la station...
}

// Observateurs Concrets
class AffichageConditions implements Observateur {
    private temperature: number = 0;
    private humidite: number = 0;

    actualiser(temperature: number, humidite: number, pression: number): void {
        this.temperature = temperature;
        this.humidite = humidite;
        this.afficher();
    }

    private afficher(): void {
        console.log(`   [AffichageConditions] Temp: ${this.temperature}°C, Humidité: ${this.humidite}%`);
    }
}

class AffichageStatistiques implements Observateur {
    private maxTemp: number = -Infinity;
    private minTemp: number = Infinity;
    private nbLectures: number = 0;

    actualiser(temperature: number, humidite: number, pression: number): void {
        this.nbLectures++;
        this.maxTemp = Math.max(this.maxTemp, temperature);
        this.minTemp = Math.min(this.minTemp, temperature);
        this.afficher();
    }

    private afficher(): void {
        console.log(`   [AffichageStats] Min/Max: ${this.minTemp}/${this.maxTemp}°C (${this.nbLectures} lectures)`);
    }
}

class AffichagePrevisions implements Observateur {
    private dernierePression: number = 1013.25;
    private prevision: string = 'Incertain';

    actualiser(temperature: number, humidite: number, pression: number): void {
        if (pression > this.dernierePression + 1) {
            this.prevision = 'Amélioration';
        } else if (pression < this.dernierePression - 1) {
            this.prevision = 'Dégradation';
        } else {
            this.prevision = 'Stable';
        }
        this.dernierePression = pression;
        this.afficher();
    }

    private afficher(): void {
        console.log(`   [AffichagePrevisions] Prévision: ${this.prevision}`);
    }
}

// --- Scénario d'utilisation ---
console.log("=== SYSTÈME D'ALERTE MÉTÉO (Observer Pattern) ===\n");

// Création du sujet (observable)
const station = new StationMeteo();

// Création des observateurs
const conditions = new AffichageConditions();
const stats = new AffichageStatistiques();
const previsions = new AffichagePrevisions();

// Abonnement des observateurs à la station
station.enregistrerObservateur(conditions);
station.enregistrerObservateur(stats);
station.enregistrerObservateur(previsions);

// Simulation de changements de mesures
station.setMesures(25, 65, 1015);
station.setMesures(26, 70, 1013);
station.setMesures(22, 90, 1010); // Dégradation attendue

// Désabonnement d'un observateur
station.supprimerObservateur(stats);

// Nouvelle mesure, sans l'observateur stats
station.setMesures(24, 75, 1012);

Événementiel

Le pattern Observer est le fondement de la programmation événementielle et réactive. Il est omniprésent :

  • UI Frameworks : Boutons qui notifient des "click listeners", champs de formulaire qui notifient des changements de valeur.
  • État d'application (Stores) : Dans des architectures comme Flux/Redux, le store est un Sujet, et les composants UI sont des Observateurs.
  • Messagerie : Les systèmes de messages (pub/sub) en sont une généralisation.
  • Modèle MVC : Le modèle (Model) notifie les vues (Views) lorsque ses données changent.

Implémentation moderne avec TypeScript (plus légère)

On peut utiliser les fonctionnalités du langage pour une approche plus légère, souvent appelée "Event Emitter".

type Ecouteur<T> = (data: T) => void;

class EventEmitter<T> {
    private ecouteurs: Ecouteur<T>[] = [];

    on(ecouteur: Ecouteur<T>): () => void {
        this.ecouteurs.push(ecouteur);
        // Retourne une fonction de désabonnement (très pratique!)
        return () => {
            const index = this.ecouteurs.indexOf(ecouteur);
            if (index > -1) this.ecouteurs.splice(index, 1);
        };
    }

    emit(data: T): void {
        // Copie du tableau pour éviter les problèmes si un écouteur se désabonne pendant l'émission
        const copieEcouteurs = [...this.ecouteurs];
        copieEcouteurs.forEach(ecouteur => ecouteur(data));
    }

    get nombreEcouteurs(): number {
        return this.ecouteurs.length;
    }
}

// Utilisation
const emitter = new EventEmitter<string>();
const desabonner = emitter.on((message) => console.log(`Reçu: ${message}`));

emitter.emit("Alerte niveau 1"); // "Reçu: Alerte niveau 1"
console.log(`Écouteurs actifs: ${emitter.nombreEcouteurs}`);

desabonner(); // Désabonnement propre
emitter.emit("Alerte niveau 2"); // (Plus rien n'est affiché)
console.log(`Écouteurs actifs: ${emitter.nombreEcouteurs}`);

Risques (fuites mémoire)

C'est le piège mortel de l'Observer. Si un Observateur ne se désabonne pas du Sujet, et que le Sujet a une durée de vie plus longue, l'Observateur ne pourra pas être garbage collecté, même s'il n'est plus utilisé ailleurs. C'est une fuite de mémoire.

class Sujet {
    private observateurs: Observateur[] = [];
    enregistrer(o: Observateur) { this.observateurs.push(o); }
    // OUBLI CRITIQUE : pas de méthode supprimerObservateur !
}

let sujet = new Sujet();
function creerObservateurFugace() {
    const obs = { actualiser: () => {} };
    sujet.enregistrer(obs); // L'observateur est retenu par le sujet.
    // La fonction se termine, mais 'obs' n'est PAS libéré car sujet le référence toujours.
    // FUITE MÉMOIRE.
}

Bonnes pratiques pour éviter les fuites :

  1. Toujours fournir un moyen de se désabonner (comme la fonction retournée par on() dans l'exemple EventEmitter).
  2. Désabonnement explicite dans les cycles de vie : Dans les frameworks UI, désabonner dans componentWillUnmount() (React), ngOnDestroy() (Angular), dispose() (divers).
  3. Utiliser des références faibles (WeakRef/WeakMap) : Dans les environnements qui le supportent (ES2021+), on peut stocker les observateurs dans un WeakMap ou un Set de WeakRef. Ainsi, si l'observateur n'est plus référencé ailleurs, il peut être collecté, et la référence faible dans le sujet devient automatiquement invalide. (Attention, cela ajoute de la complexité).
  4. Pattern "Observer mort-vivant" : Le sujet peut vérifier périodiquement si ses observateurs sont encore "vivants" (en essayant d'appeler une méthode ou en utilisant WeakRef).

Autres risques :

  • Notifications en cascade : Un observateur, en réaction à une notification, peut modifier le sujet, qui notifie à nouveau, créant une boucle infinie ou un flot incontrôlable.
  • Ordre des notifications : Non défini. Si l'ordre est important, il faut le gérer explicitement (par priorité, par exemple).
  • Performances : Notifier une liste très longue d'observateurs à chaque petit changement peut être coûteux. Solutions : debouncing, throttling, notifications batch.

Le tableau d'affichage du commissariat fonctionnait parfaitement. Chaque inspecteur réagissait à sa manière. Le pattern Observer était l'épine dorsale des systèmes réactifs, découplant parfaitement la source d'événements de ses réacteurs.

Mais il fallait manier cet outil avec précaution, sous peine de fuites mémoire silencieuses qui, à terme, feraient s'effondrer tout le système.

Le prochain pattern, Mediator, prendrait le problème à l'envers. Au lieu de laisser les objets se notifier directement (ce qui peut créer un réseau de dépendances chaotique), il centraliserait toute la communication dans un seul objet : le médiateur.