Vol. 3 · № 02 · Un atelier d'architectureAn architecture workshop

Observer, ou l'émetteur qui ignore son publicObserver, or the emitter that ignores its audience

Une fonction métier qui énumère ses réactions — notifier, tracer, réserver — soude le fait stable à la liste volatile de ceux qui y réagissent. Observer inverse l'abonnement : les intéressés s'inscrivent, l'émetteur publie un fait sans savoir qui l'écoute. Puis le fil rouge du volume — l'observateur à une méthode n'était qu'un callback — et l'avertissement : inverser la dépendance ne suffit pas si la chaîne reste synchrone.A business function that lists its reactions — notify, track, reserve — welds the stable fact to the volatile list of those who react to it. Observer inverts the subscription: the interested parties register, the emitter publishes a fact without knowing who listens. Then the volume's red thread — the one-method observer was just a callback — and the warning: inverting the dependency isn't enough if the chain stays synchronous.

AudienceAudience
Dev qui veut découpler un fait de ses réactions sans rendre le flux illisibleDev who wants to decouple a fact from its reactions without making the flow unreadable
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026June 2026
≈ 18 min●○○○Patterns d'objetsObserverTradeoff
SommaireContents ·
№ 01CadrageFraming3 min

Un émetteur qui appelle ses réactions une par une. Qui devrait varier ?An emitter that calls its reactions one by one. What should vary?

Une commande est passée : il faut notifier le client, tracer l'analytics, réserver le stock. La version qu'on écrit tous enchaîne les appels dans place(). Ça marche — et ça soude le fait métier (« une commande a été passée ») à la liste de ceux qui y réagissent. Ce qui varie ici, ce n'est pas le fait : c'est son public. Le réflexe : l'émetteur ne devrait pas connaître ses auditeurs.An order is placed: notify the customer, track analytics, reserve stock. The version we all write chains the calls inside place(). It works — and it welds the business fact ('an order was placed') to the list of those who react to it. What varies here isn't the fact: it's its audience. The reflex: the emitter shouldn't know its listeners.

Le code naïf — l'émetteur connaît tout son publicThe naive code — the emitter knows its whole audience
class OrderService:
    def place(order):
        save(order)
        EmailService().send(order.customer, "Confirmée")   # réaction 1
        Analytics().track("order_placed", order)            # réaction 2
        Warehouse().reserve(order.items)                    # réaction 3
        # … et demain : programme de fidélité, anti-fraude, webhook
Le fait vs son publicThe fact vs its audience
« commande passée »'order placed'
le fait · stablethe fact · stable
→
appellecalls
email · analytics · stock…email · analytics · stock…
le public · volatilthe audience · volatile

Le fait ne change pas ; la liste de ses réactions change tout le temps. Tant que l'émetteur les appelle nommément, ajouter une réaction oblige à rouvrir le code qui passe la commande.The fact doesn't change; the list of its reactions changes constantly. As long as the emitter calls them by name, adding a reaction forces you to reopen the code that places the order.

Le réflexe du volumeThe volume's reflex

« Quel détail je veux pouvoir changer sans toucher au reste ? » Au #1, c'était l'algorithme ; ici, c'est l'ensemble des récepteurs. Observer répond en inversant l'abonnement : ce sont les intéressés qui s'inscrivent auprès de l'émetteur, et non l'émetteur qui les énumère.'Which detail do I want to change without touching the rest?' In #1 it was the algorithm; here it's the set of receivers. Observer answers by inverting the subscription: the interested parties register with the emitter, rather than the emitter listing them.

Le pont vers l'événementiel.The bridge to event-driven.

Observer est la plus petite forme d'une grande idée : émettre un fait sans savoir qui l'écoute. La même intention, poussée au-delà du processus, devient Pub/Sub puis file de messages (Vol 4). On installe ici le réflexe en mémoire ; on le décline plus tard dans le temps et sur le réseau.Observer is the smallest form of a large idea: emit a fact without knowing who listens. The same intent, pushed beyond the process, becomes Pub/Sub then a message queue (Vol 4). We install the reflex in memory here; we extend it across time and network later.

№ 02La douleurThe pain4 min

L'émetteur devient un carrefour — et une réaction lente bloque le fait.The emitter becomes a crossroads — and one slow reaction blocks the fact.

Énumérer les réactions dans place() a deux coûts qui empirent ensemble. Le premier est structurel : l'émetteur dépend de tout ce dont chaque réaction a besoin, et il faut le rouvrir à chaque ajout. Le second est temporel : comme il les appelle en file, l'analytics lente ralentit la commande, et une réaction qui lève une exception fait échouer le fait lui-même — alors que réserver le stock n'aurait jamais dû dépendre de l'envoi d'un mail.Listing the reactions inside place() has two costs that worsen together. The first is structural: the emitter depends on everything each reaction needs, and must be reopened on every addition. The second is temporal: since it calls them in sequence, slow analytics slows the order, and a reaction that throws makes the fact itself fail — when reserving stock should never have depended on sending an email.

№ 1 · L'émetteur dépend de toutThe emitter depends on everything

place() importe l'email, l'analytics, l'entrepôt, demain l'anti-fraude. Une fonction métier centrale devient le nœud qui connaît tous les services périphériques — l'inverse du sens de dépendance qu'on veut.place() imports email, analytics, the warehouse, tomorrow anti-fraud. A central business function becomes the node that knows every peripheral service — the opposite of the dependency direction we want.

№ 2 · Le couplage temporelTemporal coupling

Les réactions sont appelées en série, dans le fil de la commande. L'analytics rame trois secondes ? Le client attend trois secondes. La décision « passer la commande » est l'otage de la lenteur de ses spectateurs.Reactions are called in series, inside the order's thread. Analytics lags three seconds? The customer waits three seconds. The decision 'place the order' is hostage to the slowness of its spectators.

№ 3 · Une réaction casse le faitOne reaction breaks the fact

Si Analytics lève une exception, l'email n'est jamais envoyé et place() échoue — la commande est perdue à cause d'un spectateur non essentiel. La défaillance d'un récepteur ne devrait pas remonter à l'émetteur.If Analytics throws, the email is never sent and place() fails — the order is lost because of a non-essential spectator. A receiver's failure should never propagate to the emitter.

Le spectateur qui fait tomber le spectacleThe spectator that brings down the show
def place(order):
    save(order)
    Analytics().track("order_placed", order)   # ✗ lève → tout s'arrête ici
    EmailService().send(...)                    # jamais atteint
    Warehouse().reserve(...)                    # jamais atteint
Le diagnostic.The diagnosis.

Le détecteur : « la défaillance ou la lenteur d'un récepteur peut-elle affecter l'émetteur ou les autres récepteurs ? ». Si oui, ils sont soudés. Le réflexe en deux temps : d'abord rendre l'émetteur aveugle à son public (le découplage structurel d'Observer) ; ensuite, isoler chaque réaction pour qu'aucune n'en compromette une autre (le découplage temporel, qui ouvre sur l'asynchrone).The detector: 'can a receiver's failure or slowness affect the emitter or the other receivers?'. If yes, they're welded. The two-step reflex: first make the emitter blind to its audience (Observer's structural decoupling); then isolate each reaction so none can compromise another (the temporal decoupling, which opens onto async).

№ 03L'abonnementThe subscription5 min

On inverse l'abonnement : l'émetteur cesse de connaître ses récepteurs.Invert the subscription: the emitter stops knowing its receivers.

Observer renverse le sens du savoir. Au lieu que l'émetteur énumère ses réactions, ce sont les réactions qui s'inscrivent auprès de lui. L'émetteur ne garde qu'une liste d'abonnés abstraits et leur publie un fait, sans savoir qui ils sont. L'animation inverse les flèches de dépendance — comme au Vol 1, mais pour le public d'un événement — puis pousse le fil rouge : l'observateur à une méthode n'était qu'une fonction.Observer reverses the direction of knowledge. Instead of the emitter listing its reactions, the reactions register with it. The emitter keeps only a list of abstract subscribers and publishes a fact to them, without knowing who they are. The animation flips the dependency arrows — as in Vol 1, but for an event's audience — then pushes the red thread: the one-method observer was just a function.

Animation — l'émetteur rendu aveugleAnimation — the emitter made blind
Départ — l'émetteur nomme son publicStart — emitter names its audience · 1/4
OrderService
le fait · stablethe fact · stable
▶
Email
appelé · couplécalled · coupled
▶
Analytics
appelé · couplécalled · coupled
▶
Stock
appelé · couplécalled · coupled
dépendance : émetteur → récepteursdependency: emitter → receivers
place() appelle Email, Analytics, Stock par leur nom. La flèche part du fait vers chaque réaction : il les connaît toutes.place() calls Email, Analytics, Stock by name. The arrow runs from the fact to each reaction: it knows them all.

Le même abonnement, deux écritures — 3 lignes à retrouverThe same subscription, two spellings — 3 lines to recall
# Objet (Observer classique)
interface Observer: on_order_placed(order)
class OrderService:    def place(order):
        save(order)
# Fonctionnel — l'observateur à une méthode EST une fonctionorders.subscribe(lambda order: analytics.track("order_placed", order))
3 ligne(s) à retrouver — écris-la de tête avant de cliquer3 line(s) to recall — write it in your head before clicking
Le motif transférable.The transferable pattern.

« Émets un fait, n'appelle pas un destinataire. » L'émetteur publie ce qui s'est passé ; qui y réagit est une décision prise ailleurs, à l'inscription. C'est le squelette de Pub/Sub (le même, par-delà le processus), de Mediator (un point central pour les relations n-à-n), et des signaux réactifs (la valeur qui notifie ses dépendants). Trois noms, une opération : découpler l'émission de la réception.'Emit a fact, don't call a recipient.' The emitter publishes what happened; who reacts is a decision made elsewhere, at subscription. It's the skeleton of Pub/Sub (the same, beyond the process), Mediator (a central point for n-to-n relations), and reactive signals (the value that notifies its dependents). Three names, one operation: decouple emission from reception.

Modèle mentalMental model
L'imageThe image

Un magazine par abonnement : la rédaction imprime le numéro et l'expédie à la liste des abonnés. Elle ne connaît pas chaque lecteur — juste « il y a des abonnés » — et n'attend pas de savoir qui l'a lu.A magazine subscription: the editorial team prints the issue and mails it to the subscriber list. It doesn't know each reader — just 'there are subscribers' — and doesn't wait to learn who read it.

La règleThe rule

Émettre un fait à une liste d'abonnés abstraits : l'émetteur ne nomme personne et ne dépend ni de leur identité ni de leur réaction.Emit a fact to a list of abstract subscribers: the emitter names no one and depends on neither their identity nor their reaction.

Où l'analogie casseWhere the analogy breaks

Le magazine part et la rédaction passe à autre chose — c'est asynchrone, sans retour attendu. L'Observer en mémoire, lui, notifie dans le même fil : si un abonné lève ou traîne, le fait en pâtit. L'analogie masque le couplage temporel qui reste — celui que le chapitre 4 démonte. Un abonnement papier ne peut pas faire échouer l'imprimerie ; un observateur synchrone, si.The magazine ships and the team moves on — it's asynchronous, no return awaited. The in-memory Observer, though, notifies on the same thread: if a subscriber throws or drags, the fact suffers. The analogy hides the temporal coupling that remains — the one chapter 4 dismantles. A paper subscription can't make the press fail; a synchronous observer can.

№ 04Le piègeThe trap4 min

Rendre l'émetteur aveugle ne suffit pas. L'Observer qui retombe en chaîne synchrone.Making the emitter blind isn't enough. The Observer that falls back into a synchronous chain.

Le piège n'est pas d'oublier la liste d'abonnés — c'est de croire qu'avoir une liste a tout réglé. Observer ne découple que le savoir : qui réagit. Il ne dit rien du temps : quand, et si la réaction réussit. Si le sujet itère ses abonnés en série, sans filet, une exception ou une lenteur d'un seul abonné remonte au fait lui-même. On a inversé la dépendance, pas isolé les réactions. Bascule le toggle : cherche le for sans try.The trap isn't forgetting the subscriber list — it's believing the list fixed everything. Observer only decouples knowledge: who reacts. It says nothing about time: when, and whether the reaction succeeds. If the subject iterates its subscribers in series, without a net, a single subscriber's exception or slowness propagates back to the fact itself. You inverted the dependency, you didn't isolate the reactions. Flip the toggle: hunt the for without a try.

# le sujet ignore qui écoute ET protège le fait de leurs défaillances
class OrderService:
    subscribers: list[Observer] = []
    def place(order):
        save(order)                       # le fait, scellé d'abord
        for o in self.subscribers:        # liste abstraite
            try: o.on_order_placed(order)
            except Exception: log(...)     # un raté n'en bloque pas un autre
# ajouter une réaction : s'abonner, ZÉRO ligne touchée dans place()
Pourquoi c'est justeWhy it's right

Le sujet scelle le fait d'abord (save), puis itère une liste abstraite où chaque réaction est isolée : un abonné qui lève est journalisé, les autres sont quand même notifiés. Le sujet ignore qui écoute et ne dépend pas de leur succès. Ajouter une réaction, c'est s'abonner — place() ne bouge pas d'une ligne. Découplé en structure et protégé dans le temps.The subject seals the fact first (save), then iterates an abstract list where each reaction is isolated: a subscriber that throws is logged, the others are still notified. The subject ignores who listens and doesn't depend on their success. Adding a reaction means subscribing — place() doesn't move a line. Decoupled in structure and protected in time.

La règle qui tranche.The rule that settles it.

Le sujet ne doit dépendre ni du type concret de ses observateurs, ni de leur succès. Le premier, Observer le règle par la liste abstraite ; le second exige un filet : sceller le fait avant de notifier, et isoler chaque réaction (try par abonné, ou mieux, une frontière asynchrone). Le test : la défaillance ou la lenteur d'un abonné peut-elle atteindre le fait ou ses voisins ? Si oui, le découplage n'est qu'à moitié fait.The subject must depend on neither the concrete type of its observers nor their success. Observer settles the first via the abstract list; the second demands a net: seal the fact before notifying, and isolate each reaction (a try per subscriber, or better, an async boundary). The test: can a subscriber's failure or slowness reach the fact or its neighbors? If yes, the decoupling is only half done.

Le piège inverse : l'auditeur fantôme.The opposite trap: the lapsed listener.

L'autre excès : s'abonner sans jamais se désabonner. Le sujet garde une référence vers un observateur qu'on croyait jeté — il survit en mémoire (fuite) et continue de réagir à des faits qui ne le concernent plus. Tout abonnement durable a besoin de son désabonnement. C'est aussi là que la frontière asynchrone (Vol 4) cesse d'être un luxe : une fois les réactions hors du fil, le découplage temporel devient structurel.The other excess: subscribing without ever unsubscribing. The subject keeps a reference to an observer you thought discarded — it survives in memory (a leak) and keeps reacting to facts that no longer concern it. Every durable subscription needs its unsubscription. It's also where the async boundary (Vol 4) stops being a luxury: once reactions leave the thread, the temporal decoupling becomes structural.

№ 05Bilan & éditoWrap-up & editorial2 min

Le public nommé dans la fonction, ou le fait publié à des abonnés.The audience named inside the function, or the fact published to subscribers.

Les deux versions, côte à côteThe two versions, side by side
appel directdirect callObserverObserver
Ajouter une réactionAdd a reactionrouvrir place()reopen place()s'abonner, émetteur intactsubscribe, emitter untouched
Qui connaît quiWho knows whoml'émetteur connaît tous les récepteursemitter knows every receiverles récepteurs connaissent le faitreceivers know the fact
DépendancesDependenciestoutes dans l'émetteurall in the emitterchacune chez son abonnéeach in its subscriber
Une réaction échoueOne reaction failsle fait échoue avec ellethe fact fails with itisolée, si chaque appel a son filetisolated, if each call has its net
Idée cléKey ideal'émetteur appelle un destinatairethe emitter calls a recipientl'émetteur publie un faitthe emitter publishes a fact

Quand ne pas introduire un ObserverWhen not to introduce an Observer

Observer a un loyer : un flux de contrôle qu'on ne lit plus en ligne droite, et un abonnement à gérer. On garde l'appel direct — et c'est correct — quand :Observer has rent: a control flow you can no longer read in a straight line, and a subscription to manage. Keep the direct call — and that's fine — when:

  • le récepteur est unique et fixe : un seul destinataire qui ne variera pas ne mérite pas une liste d'abonnés. Un appel nommé se lit mieux qu'une indirection — inverser ici, c'est de la cérémonie (YAGNI, Vol 0 #3).the receiver is single and fixed: one recipient that won't vary doesn't deserve a subscriber list. A named call reads better than an indirection — inverting here is ceremony (YAGNI, Vol 0 #3).
  • tu as besoin du résultat ou de l'ordre : Observer suppose que l'émetteur se moque de ce que font ses abonnés. Si la suite dépend de leur retour, ou exige un ordre garanti, c'est une orchestration — appelle-les explicitement, ne publie pas.you need the result or the ordering: Observer assumes the emitter doesn't care what its subscribers do. If what follows depends on their return, or requires a guaranteed order, that's orchestration — call them explicitly, don't publish.
  • le flux doit rester traçable de bout en bout : trop d'observateurs anonymes rendent le « qui a réagi à quoi » illisible. Quand comprendre une panne exige de reconstituer une chaîne invisible d'abonnés, le découplage est devenu un coût, pas un bénéfice.the flow must stay traceable end to end: too many anonymous observers make 'who reacted to what' unreadable. When understanding a failure means reconstructing an invisible chain of subscribers, the decoupling has become a cost, not a benefit.

Le réflexe n'est donc pas « émets un événement dès qu'il se passe quelque chose ». C'est : « cette liste de réactions va-t-elle changer, et le fait peut-il les ignorer — leur identité comme leur succès ? Si oui, publie un fait à des abonnés ; sinon, appelle, et garde le flux lisible. »So the reflex isn't 'emit an event whenever something happens'. It's: 'will this list of reactions change, and can the fact ignore them — their identity and their success? If yes, publish a fact to subscribers; if not, call, and keep the flow readable.'

Mot de l'éditeurFrom the editor

Observer prolonge le fil rouge du volume : l'interface à une méthode est un type de fonction déguisé. En Java de 2002, écouter un événement exigeait une interface, une classe anonyme, une méthode ; aujourd'hui on passe une lambda à subscribe(). Mais Observer porte une leçon que Strategy n'avait pas : un pattern peut découpler une dimension (qui réagit) et en laisser une autre soudée (quand, et si ça réussit). L'architecte ne s'arrête pas à « j'ai inversé la dépendance » ; il demande quelle dépendance reste.Observer extends the volume's red thread: the one-method interface is a disguised function type. In 2002 Java, listening to an event required an interface, an anonymous class, a method; today you pass a lambda to subscribe(). But Observer carries a lesson Strategy didn't: a pattern can decouple one dimension (who reacts) and leave another welded (when, and whether it succeeds). The architect doesn't stop at 'I inverted the dependency'; they ask which dependency remains.

C'est aussi le plus petit pas vers l'événementiel : émettre un fait sans savoir qui l'écoute. Poussée hors du processus, la même intention devient Pub/Sub, puis file de messages (Vol 4) — et c'est là que le découplage temporel, ici facultatif, devient la raison d'être. On installe le réflexe en mémoire ; on le décline plus tard dans le temps et sur le réseau.It's also the smallest step toward event-driven: emit a fact without knowing who listens. Pushed beyond the process, the same intent becomes Pub/Sub, then a message queue (Vol 4) — and there the temporal decoupling, optional here, becomes the whole point. We install the reflex in memory; we extend it across time and network later.

Les voisins que ce numéro débloqueThe neighbors this issue unlocks

  • Pub/Sub — le même Observer, mais l'émetteur et les abonnés ne se connaissent plus du tout : un courtier (broker) s'intercale. Au-delà du processus, c'est la file de messages du Vol 4. Observer en mémoire, Pub/Sub sur le réseau.the same Observer, but emitter and subscribers no longer know each other at all: a broker sits between them. Beyond the process, it's Vol 4's message queue. Observer in memory, Pub/Sub on the network.
  • Mediator — quand les relations deviennent n-à-n, un point central orchestre les notifications au lieu que chaque sujet tienne sa propre liste. Observer relie un fait à son public ; Mediator relie plusieurs collègues entre eux.when relations become n-to-n, a central point orchestrates the notifications instead of each subject keeping its own list. Observer links a fact to its audience; Mediator links several colleagues to each other.
  • Signaux réactifsReactive signals — la version où la valeur elle-même notifie ses dépendants : changer une donnée propage la réaction automatiquement. C'est Observer rendu implicite par le langage ou le framework — le pattern disparaît dans la déclaration.the version where the value itself notifies its dependents: changing a datum propagates the reaction automatically. It's Observer made implicit by the language or framework — the pattern vanishes into the declaration.

Le kata — écris-le, ne le relis pasThe kata — write it, don't reread it

Tu as vu l'émetteur devenir aveugle, puis l'observateur s'effondrer en callback dans l'animation du chapitre 3. Reste le geste qui ancre : l'écrire. Copie ce fichier de tests dans ton langage habituel — un sujet, subscribe, place — et fais-les passer au vert sans revenir au numéro. Le dernier test est le sournois : il vérifie qu'un abonné qui lève n'emporte pas les autres. Si ta boucle n'a pas de filet par abonné, tu auras inversé la dépendance sans isoler les réactions — le faux Observer du chapitre 4.You watched the emitter go blind, then the observer collapse into a callback in chapter 3's animation. What's left is the gesture that anchors it: writing it. Copy this test file into your usual language — a subject, subscribe, place — and turn them green without coming back to the issue. The last test is the sneaky one: it checks that a subscriber that throws doesn't take the others down. If your loop has no per-subscriber net, you'll have inverted the dependency without isolating the reactions — chapter 4's fake Observer.

test_order_events.py
# test_order_events.py — la spec EST le harnais : fais-le passer au vert.
# le sujet publie un fait à des abonnés abstraits, sans les nommer et sans
# dépendre de leur succès. un appelable = un abonné. sans lib, sans framework.

def test_l_abonne_recoit_le_fait():
    orders = OrderService()
    recu = []
    orders.subscribe(lambda o: recu.append(o.id))
    orders.place(order(id=1))
    assert recu == [1]                        # le fait a bien été publié

def test_tous_les_abonnes_dans_l_ordre():
    orders = OrderService()
    vus = []
    orders.subscribe(lambda o: vus.append("email"))
    orders.subscribe(lambda o: vus.append("analytics"))
    orders.place(order(id=1))
    assert vus == ["email", "analytics"]      # chacun, dans l'ordre d'inscription

def test_ajouter_une_reaction_ne_touche_pas_place():
    orders = OrderService()
    orders.subscribe(lambda o: None)          # on s'abonne DE L'EXTÉRIEUR, après coup…
    orders.place(order(id=1))                 # …et place() n'a pas changé d'une ligne

def test_le_desabonnement_arrete_les_notifs():
    orders = OrderService()
    vus = []
    stop = orders.subscribe(lambda o: vus.append(o.id))
    stop()                                    # subscribe rend de quoi se désabonner
    orders.place(order(id=1))
    assert vus == []                          # l'abonné parti ne reçoit plus rien

def test_un_abonne_qui_leve_n_emporte_pas_les_autres():
    # LE test du pattern : la réaction du milieu explose. Les deux autres doivent
    # quand même être notifiées, et place() ne doit PAS propager l'exception.
    orders = OrderService()
    vus = []
    orders.subscribe(lambda o: vus.append("email"))
    orders.subscribe(boom)                    # lève une exception
    orders.subscribe(lambda o: vus.append("stock"))
    orders.place(order(id=1))
    assert vus == ["email", "stock"]          # le raté est isolé, pas contagieux
Chez toi, cette semaine.In your codebase, this week.

Ouvre ta fonction métier la plus « occupée » — celle qui, après l'acte central (créer, valider, payer), enchaîne les notifications. Un point de départ : grep -rnE "\.(send|notify|track|publish|reserve)\(" src/ et repère les fonctions qui alignent trois appels ou plus après un save. Chacune soude un fait à la liste de ses réactions. Les deux questions : si demain on ajoute une réaction, rouvre-t-on cette fonction ? Et un try protège-t-il chaque appel, ou un seul raté fait-il tout tomber ?Open your busiest business function — the one that, after the central act (create, validate, pay), chains the notifications. A starting point: grep -rnE "\.(send|notify|track|publish|reserve)\(" src/ and spot the functions lining up three calls or more after a save. Each welds a fact to the list of its reactions. Two questions: if a reaction is added tomorrow, do you reopen this function? And does a try protect each call, or does one failure bring it all down?