Concept #006
Design PatternsLe Design Pattern Observer
- design-patterns
- architecture
- programmation
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 pasLe 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.