Implémentation du Pattern Observer pour les Notifications Internes
Fondamentaux JavaAnalyse approfondie de la gestion des abonnés locaux, des fuites de cycle de vie et de la gestion des erreurs dans un lecteur musical.
Dans cet article
La Problématique : Découpler l’État de la Vue
Dans un lecteur musical, le moteur audio (le Sujet) gère la position de lecture actuelle. Cependant, plusieurs composants UI—comme une barre de progression, un panneau de paroles et une icône de zone de notification—doivent réagir à ce changement. Si le moteur audio détenait des références directes vers chaque panneau, il serait étroitement couplé à la couche vue, rendant impossible l’ajout de nouveaux panneaux sans modifier le moteur.
Le pattern Observer résout cela en permettant au Sujet de maintenir une liste d’abonnés qui implémentent une interface commune. Le Sujet ne connaît pas l’identité des observateurs ; il sait seulement qu’ils peuvent être notifiés.
Exemple Concret : Synchronisation du Lecteur Musical
Voici une implémentation concrète. Nous utilisons un Set pour stocker les observateurs afin d’éviter qu’un même composant ne soit enregistré deux fois, ce qui provoquerait des mises à jour redondantes.
import java.util.*;
// Le contrat pour tout composant souhaitant des mises à jour de lecture
interface PlaybackObserver {
void onProgressUpdate(long milliseconds);
}
// Le Sujet : Gère l'état et les abonnés
class AudioEngine {
private final Set<PlaybackObserver> observers = new HashSet<>();
private long currentPosition = 0;
public void subscribe(PlaybackObserver observer) {
if (observer != null) {
observers.add(observer);
}
}
public void unsubscribe(PlaybackObserver observer) {
observers.remove(observer);
}
public void updatePosition(long newPosition) {
this.currentPosition = newPosition;
notifyObservers();
}
private void notifyObservers() {
// Création d'un instantané pour éviter ConcurrentModificationException
// si un observateur se désabonne pendant la boucle de notification
List<PlaybackObserver> snapshot = new ArrayList<>(observers);
for (PlaybackObserver observer : snapshot) {
try {
observer.onProgressUpdate(currentPosition);
} catch (Exception e) {
// Empêche un observateur défaillant de faire planter tout le moteur
System.err.println("Erreur lors de la notification : " + e.getMessage());
}
}
}
}
// Observateur concret 1 : Barre de progression
class ProgressView implements PlaybackObserver {
@Override
public void onProgressUpdate(long ms) {
System.out.println("ProgressView: Mise à jour du curseur à " + ms + "ms");
}
}
// Observateur concret 2 : Panneau de paroles
class LyricsPanel implements PlaybackObserver {
@Override
public void onProgressUpdate(long ms) {
System.out.println("LyricsPanel: Synchronisation du texte pour " + ms + "ms");
}
}
Mécanismes Critiques et Cas d’Échec
1. Le Risque de Fuite de Mémoire
Dans une application de bureau, les utilisateurs ouvrent et ferment souvent des panneaux. Si un LyricsPanel est fermé mais n’est pas explicitement retiré via unsubscribe(), l’ AudioEngine conserve une référence vers lui dans son Set. Comme le Sujet a généralement une durée de vie longue, le panneau fermé ne peut pas être récupéré par le Garbage Collector. C’est une fuite de mémoire classique. Pour éviter cela, la méthode dispose() ou close() du panneau doit appeler audioEngine.unsubscribe(this).
2. L’Ordre des Notifications
L’utilisation d’un HashSet signifie que l’ordre des notifications est non déterministe. Si le ProgressView doit être mis à jour avant le LyricsPanel, un LinkedHashSet ou un ArrayList devrait être utilisé. Cependant, compter sur l’ordre suggère souvent que les observateurs sont trop dépendants les uns des autres, ce qui contredit l’intention du pattern.
3. Isolation des Exceptions
Comme montré dans la méthode notifyObservers, l’encapsulation de l’appel dans un bloc try-catch est obligatoire. Si LyricsPanel lève une RuntimeException (par exemple, une NullPointerException lors d’un rendu UI) et que la boucle n’est pas protégée, le ProgressView ne recevra jamais la mise à jour, et l’ AudioEngine pourrait s’arrêter, coupant la musique.
Callbacks Locaux vs Architecture d’Événements Durables
Il est essentiel de distinguer ce pattern in-process d’un Message Broker (comme RabbitMQ ou Kafka).
- Pattern Observer : Synchrone, s’exécute dans le même espace mémoire JVM et est éphémère. Si l’application plante, la liste des abonnements disparaît. Utilisé pour la synchronisation UI immédiate.
- Architecture d’Événements : Asynchrone, souvent distribuée entre différents services et durable. Les événements sont persistés sur disque. Utilisé pour des flux métier (ex: “UserPurchasedSong”).
Exercice
Scénario : Vous ajoutez un VolumePanel au lecteur. Ce panneau ne doit être notifié que lorsque le volume change, et non toutes les millisecondes lors des mises à jour de progression. Comment modifier le design sans créer une classe VolumeEngine séparée ?
Réponse :
Diviser l’interface d’observateur en deux interfaces spécialisées : PlaybackObserver et VolumeObserver. L’ AudioEngine doit maintenir deux ensembles distincts : Set<PlaybackObserver> et Set<VolumeObserver>. Le VolumePanel implémenterait uniquement VolumeObserver et s’abonnerait à la liste du volume. Cela évite la “sur-notification”, où des composants sont réveillés pour des événements qui ne les concernent pas.
Cette implémentation synchrone suppose des accès sérialisés sur un thread. Le snapshot gère le retrait pendant callback, pas toute mutation concurrente ni réentrance. Capturez la valeur de l’événement si un callback rappelle updatePosition. Isoler les exceptions est notre politique, pas une obligation universelle Observer ; les callbacks précédents peuvent déjà avoir réussi. Durabilité/asynchronisme dépendent d’un broker et de sa configuration.