Aller au contenu principal

Pattern Observer

Définissez un mécanisme d'abonnement pour notifier plusieurs objets des événements qui surviennent sur l'objet qu'ils observent.


Le Problème

Vous développez une app de trading. Quand le prix d'une action change, plusieurs composants doivent réagir : le graphique se met à jour, les alertes se déclenchent, le portefeuille se recalcule.

L'approche naïve :

// stock-service.ts — le publisher connaît TOUS ses consommateurs
import { updateChart } from "./chart";
import { checkAlerts } from "./alerts";
import { recalcPortfolio } from "./portfolio";

function updateStockPrice(stock: Stock, newPrice: number) {
stock.price = newPrice;
updateChart(stock); // couplage fort
checkAlerts(stock); // couplage fort
recalcPortfolio(stock); // couplage fort
// ... chaque nouvelle feature = modifier ce fichier et ajouter un import
}

Chaque nouvel abonné = modifier le publisher. Le publisher doit connaître chaque consommateur.


La Solution

Les publishers ne connaissent pas leurs abonnés. Ils émettent juste des événements. Les abonnés s'enregistrent indépendamment :

import { createObservable } from "@pithos/core/eidos/observer/observer";

type PriceUpdate = { symbol: string; price: number };

const priceChanged = createObservable<PriceUpdate>();

// Le graphique s'abonne
priceChanged.subscribe(({ symbol, price }) => {
chart.addPoint(symbol, price);
});

// Le système d'alertes s'abonne (ne connaît pas le graphique)
priceChanged.subscribe(({ symbol, price }) => {
if (price > thresholds[symbol]) sendAlert(`${symbol} spike!`);
});

// Le portefeuille s'abonne (ne connaît ni le graphique ni les alertes)
priceChanged.subscribe(({ symbol, price }) => {
portfolio.recalculate(symbol, price);
});

// Le publisher ne sait pas qui écoute
// TS impose la forme du payload — emit({ symbol: 123 }) est une erreur de compilation
priceChanged.notify({ symbol: "AAPL", price: 150 });

Nouvel abonné ? Appelez .subscribe(). Aucune modification du publisher. Trois systèmes indépendants réagissent au même événement sans se connaître.


Démo

Code
/**
* Stock ticker using Pithos Observer pattern.
*
* The market ALWAYS runs. Subscribers receive updates only when subscribed.
* Unsubscribe = you stop receiving, but the market doesn't stop.
*/

import { createObservable } from "@pithos/core/eidos/observer/observer";
import type { Stock, StockUpdate } from "./types";
import type { Index } from "./data";
import { STOCKS as INITIAL_STOCKS, MARKET_START_HOUR, MINUTES_PER_TICK, INDICES as INITIAL_INDICES } from "./data";

export type { StockUpdate, Alert, Stock } from "./types";
export type { Index } from "./data";
export { STOCKS, ALERT_THRESHOLD, HOLDINGS, INDICES, MARKET_START_HOUR, MINUTES_PER_TICK, HISTORY_SIZE } from "./data";

/** The observable — the market emits updates, subscribers listen */
export const stockTicker = createObservable<StockUpdate>();

let tickIndex = 20;

export function getMarketTime(): string {
const totalMinutes = MARKET_START_HOUR * 60 + tickIndex * MINUTES_PER_TICK;
const h = Math.floor(totalMinutes / 60) % 24;
const m = totalMinutes % 60;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}

function randomChange(currentPrice: number): number {
return currentPrice * ((Math.random() - 0.5) * 6 / 100);
}

/** Internal market state — always running */
let marketStocks: Stock[] = INITIAL_STOCKS.map((s) => ({ ...s }));
let marketIndices: Index[] = INITIAL_INDICES.map((i) => ({ ...i }));

/** Tick the market: updates prices and notifies subscribers */
export function marketTick(): void {
tickIndex++;

marketStocks = marketStocks.map((stock) => {
const change = randomChange(stock.price);
const newPrice = Math.max(1, stock.price + change);
const changePercent = ((newPrice - stock.previousPrice) / stock.previousPrice) * 100;

// Notify subscribers
queueMicrotask(() => {
stockTicker.notify({
symbol: stock.symbol,
price: newPrice,
change: newPrice - stock.price,
changePercent,
timestamp: Date.now(),
});
});

return { ...stock, price: newPrice };
});

marketIndices = marketIndices.map((idx) => {
const pctChange = (Math.random() - 0.5) * 0.4;
return { ...idx, price: Math.max(1, idx.price + idx.price * (pctChange / 100)) };
});
}

/** Get current market indices (always live) */
export function getMarketIndices(): Index[] {
return marketIndices;
}

/** Reset everything */
export function resetMarket(): void {
tickIndex = 20;
marketStocks = INITIAL_STOCKS.map((s) => ({ ...s }));
marketIndices = INITIAL_INDICES.map((i) => ({ ...i }));
}
Result

Analogie

Les abonnements YouTube. Vous vous abonnez à une chaîne, vous êtes notifié quand elle publie. Le créateur ne sait pas combien d'abonnés vont regarder. Vous pouvez vous désabonner à tout moment. Le créateur et les spectateurs sont complètement découplés.


Quand l'Utiliser

  • Des changements dans un objet doivent déclencher des mises à jour dans d'autres
  • Vous ne savez pas à l'avance combien d'objets doivent réagir
  • Vous voulez un couplage faible entre producteurs et consommateurs d'événements

Quand NE PAS l'Utiliser

Si vous avez un seul consommateur qui réagit toujours de la même façon, un appel de fonction direct est plus clair. Observer ajoute de l'indirection qui ne vaut le coup que quand les abonnés sont dynamiques ou inconnus à la compilation.


API

  • createObservable — Créer un émetteur d'événements typé avec subscribe, notify, once, safeNotify, size et clear