Design Pattern Observateurs / Observables Vs Publish / Subscribe en PHP

Les observateurs font parti des Modèles de Conception qui visent à limiter la dépendance entre les objets. L’objectif est de permettre à un ou plusieurs objets de réagir aux messages d’autres objets, sans qu’ils ne soient connus à l’avance, sans devoir les lier « en dur » dans le code.

Nous verrons également dans ce billet le concept de Publication / Abonnement (Publish / Subscribe), concept dont l’implémentation peut être menée à bien par un système d’Observateur / Observable, sans que cela signifie que ces deux appellations soient équivalentes (comme on pourrait le penser à tord en parcourant le web).

Exposé de l’exemple

Pour illustrer notre article, nous allons partir sur un exemple simple : le cas d’un site qui publie de l’actualité, disposant d’un système de notification par mail et d’un moteur de recherche.

Nous allons tenter de développer ce site sans utiliser les modèles de conception, puis en utilisant les observateurs, puis enfin nous utiliserons le système de publication / abonnement.

Tout d’abord, réalisons une classe essentielle dans notre système : l’Article.

<?php
class Article
{
   private $_id;
   private $_titre;
   private $_resume;
   private $_redactionnel;

   private $_dbc;
   public function __construct (IDBConnector $pConnector)
   { 
      $this->_dbc = $pConnector;
   }
   public function load ($pId)
   {
      $record = $this->_connector->queryOne
              (
              "select titre, resume, redactionnel ".
              "  from articles ".
              "  where id = :id",
               array(':id'=>$pId)
              );
      foreach ($record as $key=>$value) {
         $this->{'_'.$key} = $value;
      }
   }
   public function save ()
   {
      if ($this->_id !== null) {
         $this->_connector->query
                     (
                       'update articles '.
                       ' set titre = :titre'.
                       '   resume = :resume '
                       '   redactionnel = :redactionnel '.
                       ' where id = :id',
                       array
                          (
                             ':id'=>$this->_id,
                             ':titre'=>$this->_titre,
                             ':resume'=>$this->_resume,
                             ':redactionnel'=>$this->_redactionnel,
                          )
                     );
      } else {
         $this->_connector->query
                     (
                       'insert into articles (titre, resume, redactionnel)'.
                       ' values (:titre, :resume, :redactionnel) ',
                       array
                          (
                             ':id'=>$this->_id,
                             ':titre'=>$this->_titre,
                             ':resume'=>$this->_resume,
                             ':redactionnel'=>$this->_redactionnel,
                          )
                     );
      }
   }
   //suivent les getter/setter sur les propriétés de l'article.
}

Note: J’ai ici utilisé pour la classe Article un modèle de type ActiveRecord. N’y voyez aucune préférence vis à vis des autres modèles de persistance (car d’ailleurs je préfère souvent à l’active record ses alternatives, ce dont je parlerais plus tard), mais un simple choix de concision pour l’article 🙂

Nous devons maintenant ajouter les fonctionnalités permettant

  • De notifier l’ajout / la mise à jour d’articles
  • D’ajouter / modifier nos contenus dans le moteur de recherche

Sans les modèles de conception

Le code client est responsable des opérations

<?php
//dans la page de modification d'un article, on considère le travail de filtre effectué

//chargement de l'article à modifier
$article = new Article();
$article->load($_GET['id_article']);

//mise à jour de l'article avec son nouveau contenu
$article->setTitre($_POST['titre']);
$article->setResume($_POST['resume']);
$article->setRedactionnel($_POST['redactionnel']);
$article->save();

$moteurDeRecherche = new MoteurDeRecherche();
$moteurDeRecherche->updateIndex($article->titre, $article->resume, $article->redactionnel);

$notification = new SystemeDeNotification();
$notification->sendMailForUpdate($article->id);

Le code ci dessus, bien que fortement incomplet, permet de se rendre compte d’une chose simple :
« Il va falloir à chaque appel de la méthode save de nos articles ajouter le code pour le moteur de recherche et le système de notification ».

Autre problème : si demain nous choisissons d’ajouter un nouveau comportement à l’ajout / la modification / la suppression d’un article, nous allons devoir placer ce code à chaque appelle de la méthode save.

En bref, cela fonctionne, c’est simple à comprendre, mais il existe surement plus efficace pour la phase de maintenance de l’application.

On modifie la classe Article pour qu’elle s’occupe elle même des notifications / recherches

<?php
//Dans la classe article, le cas de la méthode Save
   public function save ()
   {
      //réalisation des opérations de sauvegarde, puis
      //...
      $moteurDeRecherche = new MoteurDeRecherche();
      $moteurDeRecherche->updateIndex($this->_titre, $this->_resume, $this->_redactionnel);

      $notification = new SystemeDeNotification();
      $notification->sendMailForUpdate($this->_id);      
   }

Nous avons ici simplifié les appels du code client qui n’a plus à se soucier des opérations tierces (notification et recherche), mais nous avons malheureusement

  1. Ajouté des dépendances inutiles à notre objet Article sur le moteur de recherche et le système de notification.
  2. Rompu le principe de responsabilité unique (SRP: Single Responsibility Principle) en demandant à notre objet de s’occuper lui même des opérations d’indexation et de notification.

Pour adresser le premier problème nous pouvons injecter les dépendances sur le système de notification et le moteur de recherche (avec par exemple un setSystemeDeNotification($pNotification) et setMoteurDeRecherche($pMoteurDeRecherche)), mais le fait d’avoir rompu le SRP induit la perte en réutilisabilité de notre objet (on peut vouloir réaliser une autre application avec la même classe article sans moteur de recherche ou sans notification).

Patterns à la rescousse, Observer / Observable

Pour quelques méthodes de plus

Notre objet Article évolue et nous souhaitons pouvoir ajouter des comportements qui tracent cette évolution.

Le principe du modèle de conception Observateur / Observable permet justement à des objets d’indiquer à un autre qu’ils sont intéressés par ses évolutions.

Commençons par créer les observateurs

class ObservateurArticlePourNotification implements IObservateur
{
   public function update ($pArticle)
   {
      $notification = new SystemeDeNotification();
      $notification->sendMailForUpdate($pArticle->id);      
   }
}

class ObservateurArticlePourRecherche implements IObservateur
{
   public function update ($pArticle)
   {
      $moteurDeRecherche = new MoteurDeRecherche();
      $moteurDeRecherche->updateIndex
                                         (
                                            $this->_titre, 
                                            $this->_resume, 
                                            $this->_redactionnel
                                         );
   }
}

Nous avons développé des observateurs qui sont en charge de tâches unitaires très simples : Il nous faut maintenant modifier la classe Article pour qu’elle accepte d’être observée par ces derniers.

<?php
class Article
{
   //l'implémentation précédente, suivi des méthodes pour l'ajout d'observateurs
   //...
   private $_observers;
   public function __construct ()
   {
      //On utilise SplObjectStorage qui gèrera l'unicité 
      // des objets de la collection
      $this->_observers = new SplObjectStorage();
   }
   public function addObserver (IObservateur $pObservateur)
   {
      $this->_observers->attach($pObservateur);
   }
   public function removeObserver (IObservateur $pObservateur)
   {
      $this->_observers->detach($pObservateur);
   }

   public function save ()
   {
      //la méthode de sauvegarde actuelle, suivi de 
      //...
      foreach ($this->_observers as $observer) {
         //notification de tous les observateurs du changement
         $observer->update($this);
      }
   }
}

On constate maintenant que notre objet n’est couplé ni au système de notification ni au moteur de recherche. On constate également que la nouvelle implémentation permet d’ajouter d’autres comportements en attachant de nouveaux observateurs.

Le problème, toutefois, est qu’il faut attacher systématiquement les observateurs au sujet Article, ce qui peut être laborieux (ou nécessiter l’usage d’une fabrique qui s’assurera d’attacher les observateurs pour nous) en plus d’induire une part de lourdeur si les objets observés sont nombreux.

Avec le modèle Publication / Abonnement

Le système de Publication / Abonnement permet à l’émetteur du message de ne pas se soucier de ses observateurs car il va déléguer les tâches d’abonnement et de notification des observateurs à un tiers : l’éditeur (Publisher).

Concrètement nous allons garder des observateurs qui vont s’abonner à des publications (des messages) auprès d’un éditeur. Notre class Article, quant à elle, enverra des notifications à un seul objet : l’éditeur.

class Article 
{
   public function save () 
   {
      //la méthode save
      //..
      //On indique à l'éditeur un changement
      Publisher::notify ('article.save', $this);
   }
}

class Publisher 
{
   private static $_observers = array();

   public static function addObserver ($pMessageType, IObserver $pObserver)
   {
      if (! isset(self::$_observers[$pMessageType]))
      {
         self::$_observers[$pMessageType] = array();
      }
      self::$_observers[$pMessageType][] = $pObserver;
   }

   public static function notify ($pMessageType, $pArgs)
   {
      if (isset(self::$_observers[$pMessageType]))
      {
         foreach (self::$_observers[$pMessageType] as $observer)
         {
            $observer->update($pArgs);
         }
      }
   }
}

//les observateurs sont inchangés, ils devront simplement être enregistrés auprès du Publisher sur les bons types de message au lieu d'être enregistrés sur les Articles.

Notre objet Article récupère une dépendance sur le système de publication mais n’est plus tenu d’accueillir des observateurs, ce qui le libère de plusieurs tâches rébarbatives prises en charge par l’éditeur.

L’éditeur s’occupe seul d’accueillir les observateurs et de leur transférer les messages qu’il recevra des émetteurs.

Conclusions

N’oublions on pas que les modèles de conception ne sont pas figés.

Cet article présente des implémentation un peu trop rigides des modèles concernés (en particulier pour les observateurs ou l’on se contente d’une méthode update qui manque d’informations sur la nature du changement du sujet) et la classe Publisher un peu trop statique pour être honnête.

Le modèle de conception Publish / Subsribe est finalement une implémentation particulière du modèle de conception Observer / Observable ou l’élément observé est le Publisher.

Pour aller plus loin, vous pourrez trouver des implémentation génériques de Publish/Subscribe du coté de Symfony 2, des Zeta Components (sous l’appellation Signal / Slots,), de Jelix, ou dans d’autres langages comme python avec PySubHub.

Pour être plus complet, on parle plus souvent de Signaux / Slots lorsque les messages sont internes à une application et de Publication / Abonnement lorsque les messages sont destinés à des applications tierces, et particulièrement via des middleware comme RabitMQ ou 0MQ.

8 réflexions au sujet de « Design Pattern Observateurs / Observables Vs Publish / Subscribe en PHP »

  1. Comme d’habitude, un article clair et concis.

    Il tombe d’ailleurs parfaitement au bon moment pour moi. J’étais en train de concevoir un système équivalent de système de notification interclasse. J’étais partie sur une implémentation de type programmation Évènementielle : les classes déclenchent des évènements particulier auprès d’une classe Event, et cette dernière re-dispatche l’évènement à tous ceux qui l’écoute. Avec la possibilité d’intercepté un évènement, l’annuler…

    Mais je me demande si je suis pas partie sur un système un peu trop complexe pour implémenter des signaux.

  2. Merci de ton retour.

    Je ne sais que répondre sur ton interrogation, mais se poser la question sur la complexité du système mis en oeuvre est souvent un signe 🙂

    Pour ma part, j’essaye souvent de limiter mon enthousiasme lorsque je développe de tels systèmes (ce qui n’est pas toujours facile !).

    J’essaye, pour valider une fonctionnalité, de :
    – Imaginer plusieurs cas d’usage
    – Repenser à la fonctionnalité de base qui m’a fait développer le système à l’origine
    – Vérifier que l’usage du système reste simple pour le cas nominal

    En tout dernier lieu, si mon besoin n’est pas immédiat, j’opte souvent pour la loi du 80/20 et j’accepte que ma bibliothèque ne réalise que 80% des cas simplement, laissant au développeur client la réalisation des cas particuliers.

    Dans le pire des cas, alors que je constate que j’ai suivi une mauvaise voie, je me convainc « qui ne se plante jamais n’a aucune chance de pousser ».

  3. Bonjour,

    Bel article : clair et intéressant.

    Je n’ai jamais eu l’occasion d’utiliser ce design pattern dans mes projets, et à la lecture de cet article, je me suis dit « ça c’est un super truc, qu’il faudrait mettre en oeuvre dans des cas similaires »,

    Et puis, je me suis rendu compte que dans les exemples, il manquait l’instanciation. Et en réfléchissant a son implémentation, je me suis rendu que finalement ce design patern n’apportait rien.
    Pour oberver/observable, il manque en effet le code qui ajouter les observeurs. Il faudrait donc compléter l’exemple avec :

    $article = new Article();
    $article->load($_GET['id_article']);

    //ajout des observeurs
    $article->addObserver(new ObservateurArticlePourNotification());
    $article->addObserver(new ObservateurArticlePourRecherche());

    //mise à jour de l'article avec son nouveau contenu
    $article->setTitre($_POST['titre']);
    $article->setResume($_POST['resume']);
    $article->setRedactionnel($_POST['redactionnel']);
    $article->save();

    Et là on se rend compte qu’on retombe exactement dans le cas de figure initial qu’on voulait éviter : il faut ajouter des morceaux de code (ajout des observeurs) a chaque fois qu’on appel la méthode save (ou tout autre méthode observée)
    Certe on pouttait mettre les observeurs dans le constructeur, mais ca serait un façon détournée du deuxième cas qu’on voulait éviter ajout de dépendances sur l’objet.

    AU final, même si Observer / Observable est plus « beau » et plus « propre », il ne résoud pas les problème initiaux.

    De même pour la Publication / Abonnement, quand est-ce qu’on définis les abonnements ? On pourrait penser à le faire dans une sorte de bootstrap où tous les abonnements seraient chargés au début de l’exécution et résoudrait nos deux problèmes évoqués précédemment.
    Mais cela implique de charger tous les abonements du projet a chaque appel, même si l’appel ne lance aucune notifications.
    Cela est problématique en terme de performance pour un gros projet mélant de nombreuses classes imbriquées…

    Je suis donc assez perplexe… 🙁

    • Bonjour et merci de tes retours (désolé de répondre si tard, je retrouve tout juste une connexion internet après un long WE).

      Prenons ta première remarque
      Pour oberver/observable, il manque en effet le code qui ajouter les observeurs

      C’est en effet ce que je veux dire dans ma remarque ou j’indique qu’il reste comme problème l’ajout des observateurs, problème pouvant être résolu via l’usage d’une fabrique.

      Voici l’exemple d’une telle fabrique :


      class ArticleFactory
      {
      public function getById($pId)
      {
      $article = new Article();
      $article->load($pId)
      return $this->_attachObservers($article);
      }

      protected function _attachObservers ($pArticle)
      {
      $pArticle->addObserver(new ObservateurArticlePourNotification());
      $pArticle->addObserver(new ObservateurArticlePourRecherche());
      return $pArticle;
      }
      }

      Avec une telle fabrique, on répond tout de même aux problèmes initiaux : le code client n’a pas a se soucier des comportements tiers et notre Article reste souple dans son implémentation.

      Pour ta deuxième remarque (sur le système de Publication / Abonnement), je vais proposer une réponse plus longue avec des exemples d’implémentation complets dans un prochain billet courant de la semaine…. Stay tuned !

  4. C’est moi ou le code de l’article est mal conçu ?
    la fonction save teste la valeur d’une variable $pId avec une portée locale alors que cette derniere n’est définie nulle part ?
    Pas de mise a jour possible sur les articles , mmh ? 😉

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *