Accueil / Tutoriels / Observer

Observer

Publié le 3 Mars 2025
Conception

Observer (observateur en français) est un pattern comportemental qui va mettre en place un système d’abonnement pour des objets (les observateurs) qui pourront être notifiés lorsque des changements sont opérés sur un ou des objets particuliers (les sujets).

Le principe

Le sujet (subject) fournit des méthodes pour ajouter ou supprimer des observateurs (observers). Il possède la liste de tous les observateurs et va les notifier automatiquement dès qu’un changement de son état ou un événement survient. Les observateurs de leur côté vont implémenter une méthode (souvent nommée update()) qui sera appelée lors d’un changement du sujet. Ainsi, le couplage entre le sujet et les observateurs est très faible. On peut ajouter autant d’observateurs que nécessaire sans avoir à modifier le code du sujet.

Exemples d’utilisations

Websockets pour des notification en temps réel

Dans une application web en temps réel, le serveur peut envoyer des notifications via un websocket, qui agit ici comme le sujet. Dans une application de messagerie par exemple, si un nouveau message est reçu ou envoyé, l’interface utilisateur (ici l’observateur) sera notifiée et pourra mettre à jour son affichage.

Mode clair/sombre sur une page

Certaines pages web proposent un mode sombre. Pour que les différents composants de la page réagissent de manière cohérente, il est possible de mettre en place un “ThemeManager” qui fera office de sujet. Les différents composants de la page seront les observateurs et seront mis à jour lorsqu’un changement arrivera sur le sujet.

Points d’attentions

Un sujet peut avoir un grand nombre d’observateurs, qui seront notifiés lors du changement d’état du sujet. Même si les observateurs sont découplés du sujet, ils peuvent être interconnectés entre eux avec des dépendances. En cas de bugs ou d’effets de bord, il peut être difficile d’identifier quel(s) observateur(s) a posé problème.

En fonction des langages et structures de données utilisés, l’ordre dans lequel les observateurs sont notifiés n’est pas garanti. Si un observateur, dépend d’un autre observateur qui doit au préalable être notifié, cela peut générer des incohérences et des comportements imprévisibles.

Implémenter un observer

Le sujet déclare des méthodes pour ajouter, retirer et notifier les observateurs. Généralement, ces méthodes sont respectivement nommées subscribe(), unsubscribe() et notify(). On pourra utiliser une interface, pour définir les signatures de nos méthodes et ensuite implémenter cette interface dans une classe concrète. L’observateur lui implémente une interface qui définit la signature d’une méthode update().

Exemple d'utilisation d'Observer en Go

Voici un exemple avec Go :


package observerData


type User struct {
   Name  string
   Email string
   Phone string
}

Subject représente l'entité qui gère la liste des observateurs et envoie les notifications. Subscribe ajoute un observateur à la liste. Unsubscribe retire un observateur à la liste. Notify notifie tous les observateurs de l'événement avec les données associées.


package subject


import (
   "sync"
   "practice/designpatterns/observer/subscribers"
)


type Subject struct {
   observers [ ]subscribers.Observer
   mu        sync.Mutex
}


func (s *Subject) Subscribe(o subscribers.Observer) {
   s.mu.Lock()
   defer s.mu.Unlock()
   s.observers = append(s.observers, o)
}


func (s *Subject) Unsubscribe(o subscribers.Observer) {
   s.mu.Lock()
   defer s.mu.Unlock()


   for i, observer := range s.observers {
      if observer == o {
         // Exemple pour supprimer l'observateur en recréant la slice sans l'élément si l'ordre est important
         // s.observers = append(s.observers[:i], s.observers[i+1:]...)


         s.observers[i] = s.observers[len(s.observers)-1]
         s.observers = s.observers[:len(s.observers)-1]
         break
      }
   }
}


func (s *Subject) Notify(event string, data interface{}) {
   s.mu.Lock()
   defer s.mu.Unlock()
   // On crée une copie pour éviter les problèmes de concurrence.
   observersCopy := make([ ]subscribers.Observer, len(s.observers))
   copy(observersCopy, s.observers)


   for _, observer := range observersCopy {
      observer.Update(event, data)
   }
}

Observer définit l'interface que doivent implémenter les observateurs.


package subscribers


type Observer interface {
   Update(event string, data interface{})
}

On simule l’envoi d’un mail.


package subscribers


import (
   "log"
   "practice/designpatterns/observer/observerData"
)


type EmailObserver struct{}


func (e *EmailObserver) Update(event string, data interface{}) {
   if event == "UserRegistered" {
      if user, ok := data.(observerData.User); ok {
         log.Printf("[EmailObserver] E-mail envoyé à %s (%s)", user.Name, user.Email)
      }
   }
}

Ici on simule l’envoi d’un SMS


package subscribers


import (
   "log"
   "practice/designpatterns/observer/observerData"
)


type SMSObserver struct{}


func (s *SMSObserver) Update(event string, data interface{}) {
   if event == "UserRegistered" {
      if user, ok := data.(observerData.User); ok {
         log.Printf("[SMSObserver] SMS envoyé à %s (%s)", user.Name, user.Phone)
      }
   }
}

Notre fichier principal.


package main


import (
   "fmt"
   "log"
   "net/http"
   "practice/designpatterns/observer/observerData"
   "practice/designpatterns/observer/subject"
   "practice/designpatterns/observer/subscribers"
)


func main() {
   // Exemple de désinscription (vous pouvez l'appeler en fonction de la logique de votre application)
   // registrationSubject.Subscribe(smsObserver)


   // Configuration de l'endpoint HTTP.
   http.HandleFunc("/register", registerHandler)


   log.Println("Serveur démarré sur http://localhost:8080")
   log.Fatal(http.ListenAndServe(":8080", nil))
}


func registerHandler(w http.ResponseWriter, r *http.Request) {
   // On initialise les observers ici pour l'exemple et des raisons pratiques.
   // Dans un vrai projet on aurait un service dédié.


   // Sujet global pour la gestion de l'enregistrement des utilisateurs.
   var registrationSubject = &subject.Subject{}


   // Création des observateurs.
   emailObserver := &subscribers.EmailObserver{}
   smsObserver := &subscribers.SMSObserver{}


   // Inscription des observateurs auprès du sujet.
   registrationSubject.Subscribe(emailObserver)
   registrationSubject.Subscribe(smsObserver)


   // Code à mettre en place pour le traitement du formulaire d'inscription
   // Ici je mets des données en dur
   user := observerData.User{
      Name:  "Toto",
      Email: "toto@mail.com",
      Phone: "phone",
   }


   // Notifier tous les observateurs de l'événement d'inscription.
   registrationSubject.Notify("UserRegistered", user)


   fmt.Fprintf(w, "Utilisateur %s enregistré avec succès!", user.Name)
}

Le même exemple en PHP

Voici exactement le même exemple mais avec PHP :


namespace Practice\DesignPatterns\Observer;


interface ObserverInterface
{
   public function update(string $event, $data);
}


namespace Practice\DesignPatterns\Observer;


class User {
   public $name;
   public $email;
   public $phone;


   public function __construct(string $name, string $email, string $phone) {
       $this->name  = $name;
       $this->email = $email;
       $this->phone = $phone;
   }
}


namespace Practice\DesignPatterns\Observer;


class Subject {
   private $observers = [ ];


   // Ajoute un observateur.
   public function subscribe(ObserverInterface $observer) {
       $this->observers[ ] = $observer;
   }


   // Supprime un observateur.
   public function unsubscribe(ObserverInterface $observer) {
       foreach ($this->observers as $key => $obs) {
           if ($obs === $observer) {
               unset($this->observers[$key]);
           }
       }
   }


   // Notifie tous les observateurs de l'événement.
   public function notify(string $event, $data) {
       foreach ($this->observers as $observer) {
           $observer->update($event, $data);
       }
   }
}


namespace Practice\DesignPatterns\Observer;


class Subject {
   private $observers = [ ];


   // Ajoute un observateur.
   public function subscribe(ObserverInterface $observer) {
       $this->observers[ ] = $observer;
   }


   // Supprime un observateur.
   public function unsubscribe(ObserverInterface $observer) {
       foreach ($this->observers as $key => $obs) {
           if ($obs === $observer) {
               unset($this->observers[$key]);
           }
       }
   }


   // Notifie tous les observateurs de l'événement.
   public function notify(string $event, $data) {
       foreach ($this->observers as $observer) {
           $observer->update($event, $data);
       }
   }
}


namespace Practice\DesignPatterns\Observer;


class SMSObserver implements ObserverInterface {
   public function update(string $event, $data) {
       if ($event === "UserRegistered" && $data instanceof User) {
           error_log("[SMSObserver] SMS envoyé à {$data->name} ({$data->phone})");
       }
   }
}


declare(strict_types=1);


require "./vendor/autoload.php";


// Instanciation du sujet pour gérer l'enregistrement des utilisateurs.
use Practice\DesignPatterns\Observer\EmailObserver;
use Practice\DesignPatterns\Observer\SMSObserver;
use Practice\DesignPatterns\Observer\Subject;
use Practice\DesignPatterns\ObserverUser;


$registrationSubject = new Subject();


// Création des observateurs et inscription auprès du sujet.
$emailObserver = new EmailObserver();
$smsObserver   = new SMSObserver();
$registrationSubject->subscribe($emailObserver);
$registrationSubject->subscribe($smsObserver);


// Code à mettre en place pour le traitement du formulaire d'inscription
// Ici je mets des données en dur
$name  = "Toto";
$email = "toto@mail.com";
$phone = "phone";


// Création d'un nouvel utilisateur.
$user = new User($name, $email, $phone);


// Notification des observateurs de l'événement "UserRegistered".
$registrationSubject->notify("UserRegistered", $user);


echo "Utilisateur " . htmlspecialchars($name) . " enregistré avec succès!";

Conclusion

J'espère que grâce à cet article vous êtes désormais plus à l'aise avec le design pattern Observer. Vous pouvez retrouver mon article sur les design patterns ou les autres tutoriels sur Chain of responsibility, Decorator et Factory.