Aller au contenu principal

Concept #006

Design Patterns

Le Design Pattern Observer

#0066 min de lecture
  • design-patterns
  • architecture
  • programmation
Observer : Ne m'appelez pas, je vous appellerai !

Vous avez déjà commandé un plat au restaurant et demandé au serveur toutes les deux minutes "C'est prêt ?" Non ? Personne ne fait ça. Pourtant, c'est exactement ce que font des tonnes de programmes informatiques. On appelle ça le polling — et c'est une catastrophe.

Le pattern Observer est la solution élégante à ce problème. Son slogan : "Ne m'appelez pas, je vous appellerai !"

Le problème : le polling, ou l'art de gaspiller des ressources

Le polling, c'est vérifier en boucle si quelque chose a changé :

// Approche polling — à ne pas faire
setInterval(() => {
  const price = fetchStockPrice("AAPL");
  if (price !== lastPrice) {
    updateUI(price);
    lastPrice = price;
  }
}, 5000); // On interroge toutes les 5 secondes... utile ou pas

Le problème ? 95% des requêtes ne retournent rien de nouveau. On consomme du réseau, du CPU, de la mémoire — pour rien. Imaginez 10 000 utilisateurs qui font ça simultanément sur votre serveur.

Il existe une meilleure façon de faire : inverser la relation. Plutôt que d'aller chercher l'information, on s'abonne et on attend qu'elle vienne à nous.

Le principe : Subject et Observer

Le pattern Observer repose sur deux acteurs :

  • Le Subject (ou Observable) : la source de vérité. Il maintient une liste d'abonnés et les notifie quand son état change.
  • L'Observer : n'importe qui qui veut être tenu au courant. Il s'abonne, attend, et réagit.
Subject ──── notifie ───▶ Observer A
        ──── notifie ───▶ Observer B
        ──── notifie ───▶ Observer C

La relation est one-to-many : un sujet, potentiellement des dizaines d'observateurs. Et le sujet ne connaît pas les détails de ses observateurs — il se contente d'appeler leur méthode update().

C'est partout autour de vous

Vous utilisez ce pattern tous les jours sans le savoir :

Excel : modifiez la valeur d'une cellule A1. Instantanément, toutes les cellules qui contiennent =A1 + quelquechose se recalculent. A1 est le Subject. Les cellules dépendantes sont les Observers.

Les event listeners du DOM : quand vous écrivez button.addEventListener('click', handler), vous abonnez handler aux événements du bouton. Le bouton est le Subject.

React et la gestion d'état : useState et les librairies comme Zustand ou Redux sont des implémentations du pattern Observer. Quand le state change, les composants abonnés se re-rendent automatiquement.

Les WebSockets : votre client s'abonne à un serveur. Quand un autre utilisateur envoie un message, le serveur notifie tous les clients connectés.

Implémentation TypeScript

Voici une implémentation propre avec des types génériques :

// Les contrats (interfaces)
interface Observer<T> {
  update(data: T): void;
}
 
interface Subject<T> {
  subscribe(observer: Observer<T>): void;
  unsubscribe(observer: Observer<T>): void;
  notify(data: T): void;
}
// Le Subject : un cours de bourse en temps réel
class StockPrice implements Subject<number> {
  private observers: Set<Observer<number>> = new Set();
  private price: number = 0;
 
  subscribe(observer: Observer<number>): void {
    this.observers.add(observer);
  }
 
  unsubscribe(observer: Observer<number>): void {
    this.observers.delete(observer);
  }
 
  notify(data: number): void {
    this.observers.forEach((observer) => observer.update(data));
  }
 
  setPrice(newPrice: number): void {
    this.price = newPrice;
    this.notify(newPrice); // On notifie automatiquement
  }
}
// Les Observers : différents systèmes qui réagissent
class PriceDisplay implements Observer<number> {
  constructor(private name: string) {}
 
  update(price: number): void {
    console.log(`[${this.name}] Nouveau prix : ${price}€`);
  }
}
 
class AlertSystem implements Observer<number> {
  constructor(private threshold: number) {}
 
  update(price: number): void {
    if (price < this.threshold) {
      console.log(`ALERTE : prix sous ${this.threshold}€ ! (${price}€)`);
    }
  }
}
// Mise en pratique
const appleStock = new StockPrice();
 
const dashboard = new PriceDisplay("Dashboard");
const mobileApp = new PriceDisplay("App Mobile");
const alertBot = new AlertSystem(150);
 
appleStock.subscribe(dashboard);
appleStock.subscribe(mobileApp);
appleStock.subscribe(alertBot);
 
appleStock.setPrice(172);
// [Dashboard] Nouveau prix : 172€
// [App Mobile] Nouveau prix : 172€
 
appleStock.setPrice(148);
// [Dashboard] Nouveau prix : 148€
// [App Mobile] Nouveau prix : 148€
// ALERTE : prix sous 150€ ! (148€)
 
// Désabonnement propre
appleStock.unsubscribe(mobileApp);

Une version fonctionnelle avec EventEmitter

En pratique, on préfère souvent une API événementielle plus flexible :

type Listener<T> = (data: T) => void;
 
class EventEmitter<Events extends Record<string, unknown>> {
  private listeners: {
    [K in keyof Events]?: Set<Listener<Events[K]>>;
  } = {};
 
  on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void {
    if (!this.listeners[event]) {
      this.listeners[event] = new Set();
    }
    this.listeners[event]!.add(listener);
 
    // Retourne une fonction de désabonnement (cleanup)
    return () => this.off(event, listener);
  }
 
  off<K extends keyof Events>(event: K, listener: Listener<Events[K]>): void {
    this.listeners[event]?.delete(listener);
  }
 
  emit<K extends keyof Events>(event: K, data: Events[K]): void {
    this.listeners[event]?.forEach((listener) => listener(data));
  }
}
 
// Utilisation typée
type StockEvents = {
  priceChanged: { symbol: string; price: number };
  marketClosed: { reason: string };
};
 
const emitter = new EventEmitter<StockEvents>();
 
const unsubscribe = emitter.on("priceChanged", ({ symbol, price }) => {
  console.log(`${symbol}: ${price}€`);
});
 
emitter.emit("priceChanged", { symbol: "AAPL", price: 172 });
 
// Nettoyage
unsubscribe();

Avantages et pièges

Les avantages :

  • Découplage : le Subject ne connaît pas ses Observers. On peut en ajouter ou en supprimer sans toucher au Subject.
  • Open/Closed Principle : on étend le comportement sans modifier le code existant.
  • Réactivité : zéro polling, zéro gaspillage. La notification arrive au bon moment.

Les pièges à éviter :

Memory leaks — le piège classique. Si vous ne vous désabonnez pas, l'Observer reste en mémoire même si plus rien ne le référence :

// Dans un composant React, toujours nettoyer
useEffect(() => {
  const unsubscribe = store.subscribe(handleChange);
  return unsubscribe; // Cleanup au démontage du composant
}, []);

L'ordre des notifications n'est pas garanti. Si vos Observers ont des dépendances entre eux, vous allez avoir des surprises.

Les cascades de notifications : un Observer qui modifie le Subject dans son update() peut déclencher une boucle infinie. Soyez vigilant.

Quand l'utiliser

Le pattern Observer est le bon choix quand :

  • Plusieurs parties du système doivent réagir à un même événement sans se connaître entre elles.
  • Vous voulez éviter du polling coûteux sur des ressources qui changent de façon imprévisible.
  • Vous construisez un système d'événements, un store de state, ou n'importe quelle architecture publish/subscribe.

En revanche, si vous n'avez qu'un seul Observer ou si les relations sont simples et stables, un appel direct suffit. Ne sur-engineerez pas.


Le pattern Observer est l'un des plus utilisés en développement — souvent sans qu'on le sache. Derrière chaque addEventListener, chaque hook React, chaque store Redux, il y a ce même principe : s'abonner une fois, et laisser le Subject faire le travail.