MFormations
Modern Design Patterns

Chapitre 8

08 — Patterns Comportementaux : Observer & Mediator

08 — Patterns Comportementaux : Observer & Mediator

Chapitre 08 : Observer & Mediator

Durée estimée : 4 séances de 3h Objectifs : Maîtriser les patterns Observer et Mediator, savoir les implémenter en TS/Java/PHP/Go, comprendre leurs variantes modernes (RxJS, Redux, EventEmitter)


Partie 1 : Le Pattern Observer

1.1 Définition et Intention

Le pattern Observer est un pattern comportemental qui établit une relation un-à-plusieurs entre un objet (le sujet) et ses dépendants (les observateurs). Quand le sujet change d'état, tous les observateurs sont automatiquement notifiés.

Intention du GoF : "Définir une dépendance un-à-plusieurs entre objets, de sorte que lorsqu'un objet change d'état, tous ses dépendants soient notifiés et mis à jour automatiquement."

1.2 Problème résolu

Supposons un système de météo. Plusieurs afficheurs (mobile, desktop, tableau de bord) doivent être mis à jour quand la température change.

Sans Observer : Chaque afficheur doit polling régulièrement ou le service météo doit connaître chaque afficheur — violation ouverte du principe Open/Closed.

Avec Observer : Le service météo notifie tous les abonnés sans connaître leurs détails.

1.3 Structure UML

Diagramme en cours de génération...

1.4 Implémentation pas à pas

1.4.1 Version simple (TypeScript)

interface Observer {
    update(data: any): void;
}

class Subject {
    private observers: Observer[] = [];
    private state: any;

    attach(observer: Observer): void {
        this.observers.push(observer);
    }

    detach(observer: Observer): void {
        const index = this.observers.indexOf(observer);
        if (index !== -1) this.observers.splice(index, 1);
    }

    notify(): void {
        for (const observer of this.observers) {
            observer.update(this.state);
        }
    }

    setState(state: any): void {
        this.state = state;
        this.notify();
    }

    getState(): any {
        return this.state;
    }
}

class ConcreteObserver implements Observer {
    constructor(private name: string) {}

    update(data: any): void {
        console.log(`[${this.name}] received update:`, data);
    }
}

// Usage
const subject = new Subject();
const obs1 = new ConcreteObserver("Observer 1");
const obs2 = new ConcreteObserver("Observer 2");

subject.attach(obs1);
subject.attach(obs2);
subject.setState({ temperature: 25 });

1.4.2 Version fonctionnelle

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

class EventEmitter<T> {
    private listeners: Set<Listener<T>> = new Set();

    on(listener: Listener<T>): () => void {
        this.listeners.add(listener);
        return () => this.listeners.delete(listener);
    }

    emit(data: T): void {
        this.listeners.forEach(listener => listener(data));
    }

    off(listener: Listener<T>): void {
        this.listeners.delete(listener);
    }
}

1.5 Variante : Property Change Support

Pattern JavaBeans standardisé :

import java.beans.PropertyChangeSupport;
import java.beans.PropertyChangeListener;

public class WeatherData {
    private PropertyChangeSupport support = new PropertyChangeSupport(this);
    private float temperature;

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener(listener);
    }

    public void setTemperature(float newTemp) {
        float oldTemp = this.temperature;
        this.temperature = newTemp;
        support.firePropertyChange("temperature", oldTemp, newTemp);
    }
}

1.6 Observer en PHP (Laravel Events)

<?php
// App\Events\OrderShipped.php
class OrderShipped implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public Order $order
    ) {}

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('orders.'.$this->order->id),
        ];
    }
}

// App\Listeners\SendShipmentNotification.php
class SendShipmentNotification
{
    public function handle(OrderShipped $event): void
    {
        Mail::to($event->order->user)->send(
            new OrderShippedMail($event->order)
        );
    }
}

// Registration dans EventServiceProvider
protected $listen = [
    OrderShipped::class => [
        SendShipmentNotification::class,
        UpdateOrderStatus::class,
    ],
];

1.7 Observer avec RxJS

import { Observable, Subject } from 'rxjs';
import { map, filter, debounceTime } from 'rxjs/operators';

// Création d'un Observable
const observable = new Observable<number>(subscriber => {
    let count = 0;
    const interval = setInterval(() => {
        subscriber.next(count++);
        if (count > 10) {
            subscriber.complete();
            clearInterval(interval);
        }
    }, 1000);
});

// Subject (Observer + Observable)
const subject = new Subject<number>();

subject.pipe(
    filter(n => n % 2 === 0),
    map(n => `Even: ${n}`)
).subscribe(console.log);

subject.next(1);
subject.next(2);
subject.next(3);

1.8 Observer avec Redux

Redux adapte Observer au pattern unidirectional data flow :

import { createStore, Store } from 'redux';

interface AppState {
    count: number;
}

type Action = { type: 'INCREMENT' } | { type: 'DECREMENT' };

function reducer(state: AppState = { count: 0 }, action: Action): AppState {
    switch (action.type) {
        case 'INCREMENT': return { count: state.count + 1 };
        case 'DECREMENT': return { count: state.count - 1 };
        default: return state;
    }
}

const store: Store<AppState> = createStore(reducer);

// Observer pattern: subscribe to store changes
const unsubscribe = store.subscribe(() => {
    console.log('State changed:', store.getState());
});

store.dispatch({ type: 'INCREMENT' }); // logs: State changed: { count: 1 }

Partie 2 : Le Pattern Mediator

2.1 Définition et Intention

Le Mediator est un pattern comportemental qui réduit le couplage entre les composants en les faisant communiquer indirectement via un objet médiateur.

Intention du GoF : "Définir un objet qui encapsule la façon dont un ensemble d'objets interagissent. Le Mediator promeut un couplage faible en empêchant les objets de se référer explicitement les uns aux autres, et permet de faire varier leurs interactions indépendamment."

2.2 Problème résolu

Dans une interface utilisateur avec des boutons, listes, champs de texte, chaque composant peut devoir réagir aux actions des autres. Sans médiateur, chaque composant doit connaître tous les autres → couplage en N².

Diagramme en cours de génération...

2.3 Structure UML

Diagramme en cours de génération...

2.4 Implémentation TypeScript

interface Mediator {
    notify(sender: Component, event: string): void;
}

class Component {
    constructor(protected mediator: Mediator) {}
}

class Button extends Component {
    click(): void {
        console.log('Button clicked');
        this.mediator.notify(this, 'click');
    }
}

class TextBox extends Component {
    private value: string = '';

    setValue(value: string): void {
        this.value = value;
        this.mediator.notify(this, 'input');
    }

    getValue(): string {
        return this.value;
    }
}

class ListBox extends Component {
    private items: string[] = [];

    addItem(item: string): void {
        this.items.push(item);
        this.mediator.notify(this, 'itemAdded');
    }

    getItems(): string[] {
        return this.items;
    }
}

class DialogMediator implements Mediator {
    private button: Button;
    private textBox: TextBox;
    private listBox: ListBox;

    constructor() {
        this.button = new Button(this);
        this.textBox = new TextBox(this);
        this.listBox = new ListBox(this);
    }

    notify(sender: Component, event: string): void {
        if (sender === this.textBox && event === 'input') {
            this.listBox.addItem(this.textBox.getValue());
        }
        if (sender === this.button && event === 'click') {
            console.log('Items:', this.listBox.getItems());
        }
    }

    getButton(): Button { return this.button; }
    getTextBox(): TextBox { return this.textBox; }
}

2.5 Mediator en Go

package main

import "fmt"

type Mediator interface {
    Notify(sender Component, event string)
}

type Component struct {
    mediator Mediator
}

func NewComponent(m Mediator) Component {
    return Component{mediator: m}
}

type Button struct {
    Component
}

func (b *Button) Click() {
    fmt.Println("Button clicked")
    b.mediator.Notify(b, "click")
}

type TextBox struct {
    Component
    value string
}

func (t *TextBox) SetValue(v string) {
    t.value = v
    t.mediator.Notify(t, "input")
}

func (t *TextBox) Value() string { return t.value }

type Dialog struct {
    button  *Button
    textBox *TextBox
}

func NewDialog() *Dialog {
    d := &Dialog{}
    d.button = &Button{Component: NewComponent(d)}
    d.textBox = &TextBox{Component: NewComponent(d)}
    return d
}

func (d *Dialog) Notify(sender Component, event string) {
    switch s := sender.(type) {
    case *TextBox:
        if event == "input" {
            fmt.Println("Dialog: text changed to", s.Value())
        }
    case *Button:
        if event == "click" {
            fmt.Println("Dialog: button was clicked")
        }
    }
}

2.6 Event Bus comme Mediator

type EventHandler = (...args: any[]) => void;

class EventBus {
    private handlers: Map<string, EventHandler[]> = new Map();

    on(event: string, handler: EventHandler): void {
        if (!this.handlers.has(event)) {
            this.handlers.set(event, []);
        }
        this.handlers.get(event)!.push(handler);
    }

    off(event: string, handler: EventHandler): void {
        const handlers = this.handlers.get(event);
        if (handlers) {
            const index = handlers.indexOf(handler);
            if (index !== -1) handlers.splice(index, 1);
        }
    }

    emit(event: string, ...args: any[]): void {
        const handlers = this.handlers.get(event);
        if (handlers) {
            handlers.forEach(handler => handler(...args));
        }
    }
}

// Usage
const bus = new EventBus();
bus.on('user:created', (user) => {
    console.log('Send welcome email to', user.email);
});
bus.on('user:created', (user) => {
    console.log('Initialize workspace for', user.name);
});
bus.emit('user:created', { name: 'Alice', email: 'alice@example.com' });

Partie 3 : Observer vs Mediator vs Autres Patterns

3.1 Tableau Comparatif

CritèreObserverMediatorPub/Sub
Communication1→N directN→M via médiateurN→M via courtier
CouplageSujet/ObservateurComposants/MédiateurComplet découplé
ConnaissancesSujet connaît obs.Composants connaissent médiateurAucune
ScalabilitéFaible (N obs.)MoyenneTrès haute
ComplexitéFaibleMoyenneÉlevée
Exemple typiqueEvent listenersUI ComponentsRabbitMQ, Kafka

3.2 Quand utiliser quel pattern ?

Observer quand :

  • Un objet doit notifier plusieurs autres sans connaître leur structure
  • Le nombre d'observateurs est dynamique
  • On veut un découplage unidirectionnel

Mediator quand :

  • Plusieurs objets doivent communiquer de façon complexe
  • On veut centraliser la logique de coordination
  • On veut réduire les dépendances N→N en 1→1

3.3 Anti-patterns

Observer :

  • Fuite mémoire : oublier de détacher les observateurs
  • Notification excessive : trop d'événements tuent les performances
  • Dépendances cycliques : A notifie B qui notifie A

Mediator :

  • God Object : le médiateur devient trop complexe
  • Performance : goulot d'étranglement central
  • Debugging difficile : flot de contrôle indirect

Partie 4 : Implémentations Modernes

4.1 Observer Pattern avec Proxy (ES6)

function createObservable<T extends object>(target: T): [T, () => void] {
    const handlers: Set<() => void> = new Set();
    
    const proxy = new Proxy(target, {
        set(obj, prop, value) {
            obj[prop as keyof T] = value;
            handlers.forEach(handler => handler());
            return true;
        }
    });

    const subscribe = (handler: () => void) => {
        handlers.add(handler);
        return () => handlers.delete(handler);
    };

    return [proxy, subscribe] as unknown as [T, () => void];
}

// Usage
const [state, subscribe] = createObservable({ count: 0 });
const unsubscribe = subscribe(() => console.log('State changed:', state));
state.count = 1; // logs: State changed: 1
unsubscribe();

4.2 Observer en Go avec channels

package main

import "fmt"

type Observer interface {
    Update(message string)
}

type Subject struct {
    observers map[string]Observer
}

func NewSubject() *Subject {
    return &Subject{observers: make(map[string]Observer)}
}

func (s *Subject) Attach(id string, obs Observer) {
    s.observers[id] = obs
}

func (s *Subject) Detach(id string) {
    delete(s.observers, id)
}

func (s *Subject) Notify(message string) {
    for _, obs := range s.observers {
        obs.Update(message)
    }
}

// Utilisation de channels pour notifier
type ChannelObserver struct {
    ch chan string
}

func (o *ChannelObserver) Update(message string) {
    o.ch <- message
}

func main() {
    subject := NewSubject()
    obs := &ChannelObserver{ch: make(chan string, 10)}
    subject.Attach("obs1", obs)
    
    go func() {
        for msg := range obs.ch {
            fmt.Println("Received:", msg)
        }
    }()
    
    subject.Notify("Hello observers!")
}

4.3 Reactive Streams (Java)

import java.util.concurrent.Flow.*;
import java.util.concurrent.SubmissionPublisher;

public class ReactiveExample {
    public static void main(String[] args) throws InterruptedException {
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();
        
        Subscriber<Integer> subscriber = new Subscriber<>() {
            private Subscription subscription;
            
            @Override
            public void onSubscribe(Subscription subscription) {
                this.subscription = subscription;
                subscription.request(1); // Request first item
            }
            
            @Override
            public void onNext(Integer item) {
                System.out.println("Received: " + item);
                subscription.request(1); // Request next item
            }
            
            @Override
            public void onError(Throwable throwable) {
                System.err.println("Error: " + throwable);
            }
            
            @Override
            public void onComplete() {
                System.out.println("Complete!");
            }
        };
        
        publisher.subscribe(subscriber);
        publisher.submit(1);
        publisher.submit(2);
        publisher.submit(3);
        publisher.close();
        
        Thread.sleep(100);
    }
}

Partie 5 : Exercices et Cas Pratiques

5.1 Implémenter un système de chat

Utiliser Mediator pour coordonner les messages entre utilisateurs :

Diagramme en cours de génération...

5.2 Implémenter un panier d'achat réactif

Utiliser Observer pour synchroniser :

  • Quantité dans le panier
  • Total calculé
  • Badge de notification
  • Stock réservé

5.3 Quiz de vérification

Voir quiz/README.md pour 20 QCM.


Résumé

  • Observer : Relation 1→N, notification push, idéal pour event systems
  • Mediator : Centralisation des interactions, réduction du couplage
  • Modernes : RxJS, Redux, EventEmitter, Reactive Streams
  • Pièges : Fuites mémoire, God Object, debugging complexe

Prochain chapitre : Strategy & Template Method — algorithmes interchangeables.