Free online content available in this course.

You can get support and mentoring from a private teacher via videoconference on this course.

Got it!

Last updated on 12/6/13

Le patron de conception Observateur (Observer)

Log in or subscribe for free to enjoy all this course has to offer!

Pour bien comprendre pourquoi et comment utiliser un design pattern, il faut une raison. Je vais donc vous définir un sujet particulier sur lequel vous devrez cogiter. Oui, vous avez bien lu, il va falloir réfléchir. Le meilleur moyen de retenir durablement ce qui va suivre est de buter sur le problème. Pour ma part je suivrai un cheminement pour essayer progressivement de m'approcher de la solution.

Posons le problème

Soit un ascenseur, sa fonction première est de monter ou de descendre des étages pour satisfaire des requêtes de personnes. Comme vous le savez peut être, l'installation d'un ascenseur ne se réduit pas à placer la cabine qui se déplacera dans un rail. De multiples capteurs sont présents pour que la cabine puisse se comporter idéalement selon les situations, en voici une petite liste :

  • Un capteur qui indique qu'un utilisateur ne se retrouve pas entre deux niveaux lorsque les portes doivent s'ouvrir.

  • Un capteur situé au niveau des portes pour savoir si quelqu'un n'est pas totalement rentré dans l'ascenseur.

  • Un capteur qui mesure la pression exercée sur les portes de l'ascenseur lorsqu'elles sont ouvertes. Ce dispositif permet de s'assurer que rien ne sera écrasé si une trop forte résistance s'oppose lors de la fermeture.

Cette liste n'est pas exhaustive, mais c'est pour vous montrer que sous son apparence simpliste, un ascenseur comporte des mécanismes complexes (sans parler du traitement des requêtes utilisateurs).

Par cette brève introduction, rentrons maintenant dans le vif du sujet. Vous êtes en charge de gérer la bonne position de la cabine de l'ascenseur. Pour cela, la cabine possède diverses informations comme son sens courant de déplacement et son étage actuel. Le seul moyen pour vous de savoir quand l'ascenseur changera d'étage sera par l’intermédiaire d'un capteur photosensible placé sur l'ascenseur et détectant un repère placé sur chaque étage. Comme un schéma est souvent plus clair pour montrer de quoi il est question, je vous propose le suivant :

Récapitulatif ascenseur

Dès que le laser passe sur le repère, une variation va être détectée. Le capteur laser sait donc qu'un étage a été décelé.
La cabine doit maintenant mettre à jour son nouvel étage courant.

Voici les éléments qui sont donnés pour résoudre ce problème :

Elements UML

Vous pouvez voir que le capteur est dans un thread, cela importe peu, concentrez-vous juste sur les opérations à faire lorsqu'une variation est détectée.
En gros, ne touchez à rien d'autre dans la méthode du thread que le corps de la boucle.

Avec ces éléments de base, vous pouvez modifier les classes à votre souhait, rajouter autant de méthodes que vous le souhaitez, l'essentiel étant de faire diffuser l'information du capteur vers la cabine et de mettre à jour l'étage de celle-ci. A vos brouillons !

Résolvons progressivement notre cas

Comme dit précédemment dans ce tutoriel, je vais décrire plusieurs approches qui sont issues de raisonnements simples pour montrer comment doit s'organiser votre pensée. Vous verrez qu'il s'agit de l'approche de la réussite par l'échec. Si vous avez suffisamment assimilé le sujet, deux voire trois options vous sont peut-être venues à l'esprit, je vais tenter de décrire chacune d'entre-elles et les confronter aux principes objets que j'ai évoqué dans le chapitre précédent.

Première approche, scruter le capteur pour déterminer une variation :
On peut partir du principe que le capteur vérifie en permanence le repère d'un étage. Avec cette approche, il suffit de créer une méthode renvoyant un booléen qui indique si oui ou non une variation a été détectée. Voyons une version de ce diagramme UML :

Diagramme cas 1

Il ne manque plus qu'à vérifier que la méthode detecteEtage() renvoie vrai.
Oui dans les faits, c'est une manière plutôt élégante de résoudre ce problème, vous respectez même les grands principes objets. En effet, la cabine peut juste consulter l'état d'un objet sans en altérer le comportement, aussi chaque objet peut évoluer indépendamment. En rajoutant un peu d'abstraction on pourrait même se payer le luxe de pouvoir représenter n'importe quel capteur qui signale juste un changement d'état. Comment ? à l'aide d'une interface bien sûr, je vous laisse réfléchir sur le diagramme suivant :

Diagramme cas 1, abstraction

Voilà une conception très flexible, si on décide de changer de capteur (par exemple un capteur à aimantation), il suffira de créer une nouvelle classe qui implémente l'interface. Le code pour la cabine ne changera pas.
Cependant cette approche a un problème et vous avez probablement remarqué que je n'en ai pas parlé. Quel est le code exact à mettre dans la classe Cabine ? Puisqu'on doit scruter les changements d'états du capteur, il faut pouvoir connaître en permanence l'état courant de celui-ci. Si vous avez deviné, oui il s'agit bien d'une boucle qui s'occupe de vérifier la variation. Et c'est encore une boucle infinie comme pour le capteur et bloquante de surcroît si on ne la place pas dans un autre thread. L'implémentation est donc assez fastidieuse dans la cabine mais elle est possible. L’inconvénient majeur est que malgré tout, on va consommer des ressources processeur inutilement et il faudra éventuellement gérer les problèmes de synchronisme de thread. Imaginez que le capteur détecte toutes les 60èmes de seconde, il est important que la cabine consulte au minimum 60 fois par seconde aussi.

Retenez que cette solution n'est pas mauvaise en soit mais pour des raisons de performances/complexités, elle n'est pas spécialement adaptée à nos besoins. C'est pourquoi nous l'abandonnons pour un autre raisonnement tout aussi trivial. Voyez tout de même la démarche adoptée pour réussir à pouvoir définir un comportement commun à certains capteurs, ce qui fait que l'écriture du thread pour la cabine n'aurait pas changé si on "substituait" un autre capteur au même comportement. Adopter cette démarche est la voie de la programmation réutilisable et flexible.

Seconde approche, avoir l'instance de l'ascenseur dans la classe capteur :
Une autre solution serait tout simplement de prévenir la cabine qu'une variation a été détectée. Cette méthode est beaucoup plus efficace et ne comporte pas de défaut particulier. Il faut juste que la cabine puisse proposer une méthode sur laquelle on pourra informer un changement d'étage. Comme à mon habitude, je vous propose un diagramme UML de cette solution :

Diagramme cas 2

A vrai dire, si vous modélisez le problème de cette manière, vous vous passez d'une deuxième boucle comme traitée dans la première approche et vous respectez par la même occasion les grands principes objets. En fait pour tout vous dire, c'est une implémentation concrète du design pattern observateur. Le seul inconvénient majeur de notre capteur est que si nous voulons ajouter une autre classe qui souhaite savoir qu'une variation a eu lieu, il va falloir rajouter une référence explicite dans le capteur laser et rajouter l'instruction dans notre boucle de thread.

Diagramme cas 2, multiple

On pourrait imaginer que la cabine contrôle le moteur. Admettons que pour offrir une sécurité supplémentaire, ce moteur vérifie indépendamment si l'ascenseur n'est pas à un niveau hors des bornes (13ème étage par exemple alors que le bâtiment ne possède que 12 étages).
Dans ce cas précis, on voit bien que le code du capteur va devoir être modifié pour renseigner cette information au moteur. Rappelez-vous qu'une bonne conception doit être fermée autant que possible aux modifications sur le code existant. Si vous avez suivi mon discours jusqu'à présent, vous pouvez déjà entrevoir une solution à l'aide d'interfaces, cette abstraction qui permettra au capteur laser de signaler à un nombre quelconque de classes sa variation sans modifier le code existant se rapprochera du patron de conception observateur. Voyons maintenant de quoi il est question.

L'observateur

Ce que nous cherchons à faire ici se résume simplement avec un schéma :

Représentation observateur

Transposé à notre cas, l’émetteur d'une information se révélerait être le capteur. Les récepteurs de signalement seraient la cabine ainsi que le moteur.

Je vous ai mis plusieurs couples de mots qui peuvent aider certaines personnes à comprendre dans quel cas nous pouvons appliquer le problème. Notez que les termes "Observable" et "Observateur" sont les mots les plus représentatifs car ils sont plus abstraits (assez difficile à juger tout de même). Cependant vous ne serez pas en tort si vous utilisez les autres termes ;) .

Grâce à cette représentation, on peut directement constater quels termes vont apparaître dans notre conception pour manipuler des abstractions. Il y a cependant un détail à noter : Un annuaire, pour prévenir ses abonnés qu'un changement d'état existe (on parle de notification), doit contenir la liste des intéressés. Ce qui implique que l'annuaire doit proposer des méthodes permettant à n'importe qui de s'inscrire et de se désinscrire aux moments souhaités.

L'observateur lui, se contente de s'inscrire à ce qui l'intéresse, il a juste besoin d'être prévenu lors d'une parution. Ce sera donc une interface avec la méthode permettant de notifier.

UML esquisse observeur

La méthode notification() permet de signaler un changement de la part de l'observable.
Nous avons notre version minimale du patron de conception Observateur ! Voyez que les meilleures solutions ne sont pas forcément les plus compliquées (c'est même souvent le contraire). Il a suffi d'une classe et d'une interface pour pouvoir modéliser ce comportement. En l'appliquant à notre sujet de départ voici le diagramme de classes UML que l'on obtiendrait :

UML final

Passons maintenant à notre implémentation (je ne vais que m'attarder sur la partie code sur l'observateur et l'observable) :

#include <string>
#include <list>
#include <iostream>
using namespace std;

class IObservateur
{
  public:
    virtual void notifier() = 0;
};

class Observable
{
  public:
    void notifierObservateurs() const
    {
        // Notifier tous les observateurs
        list<IObservateur*>::const_iterator end = m_observateurs.end();
        for (list<IObservateur*>::const_iterator it = m_observateurs.begin(); it != end; ++it)
            (*it)->notifier();
    }

    void ajouterObservateur(IObservateur* observateur)
    {
        // On ajoute un abonné à la liste en le plaçant en premier (implémenté en pull).
        // On pourrait placer cet observateur en dernier (implémenté en push, plus commun).
        m_observateurs.push_front(observateur);
    }

    void supprimerObservateur(IObservateur* observateur)
    {
        // Enlever un abonné a la liste
        m_observateurs.remove(observateur);
    }
    
  private:
        list<IObservateur*> m_observateurs;
};

class Cabine : public IObservateur
{
  public:
    void notifier()
    {
        cout << "Cabine a recu la notif" << endl;
        // Changement d'étage selon son sens et sa position précédente.
    }
};

class Moteur : public IObservateur
{
  public:
    void notifier()
    {
        cout << "Moteur a recu la notif" << endl;
        // Verification que l'étage soit dans les bornes autorisées.
    }
};

class CapteurLaser : public Observable
{
  public:

   // Le code de la boucle while en environnement Threadé
   void run()
   {
       while(true)
       {
           if(m_detecteVariation)
               notifierObservateurs();
       }
   }

   private:
     bool m_detecteVariation;
};

int main()
{
    Cabine instanceCabine;
    Moteur instanceMoteur;

    CapteurLaser capteurEtage;

    capteurEtage.ajouterObservateur(&instanceCabine);
    capteurEtage.ajouterObservateur(&instanceMoteur);

    // On simule manuellement une variation (normalement c'est le thread qui s'en charge)
    capteurEtage.notifierObservateurs();

    // La cabine et le moteur ont reçu une notification sur leur méthode notifier()

    capteurEtage.supprimerObservateur(&instanceMoteur);
    cout << "Suppression du moteur dans les abonnes" << endl;

    capteurEtage.notifierObservateurs();


    return 0;
}

Et le java correspondant :

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

interface IObservateur
{
	public void notifier();
}

class Observable
{

	public Observable() 
	{
		m_observateurs = new LinkedList<IObservateur>();
	}
	
	public void notifierObservateurs()
	{
	   Iterator<IObservateur> it = m_observateurs.iterator();
	    // Notifier tous les observers
	   while(it.hasNext()){
		   IObservateur obs = it.next();
		   obs.notifier();
	   }
	}
	
	void ajouterObservateur(IObservateur observateur)
	{
	    // On ajoute un abonné à la liste en le plaçant en premier (implémenté en pull).
            // On pourrait placer cet observateur en dernier (implémenté en push, plus commun).
	    m_observateurs.add(observateur);
	}
	
	void supprimerObservateur(IObservateur observateur)
	{
	    // Enlever un abonné a la liste
	    m_observateurs.remove(observateur);
	}
	
	private List<IObservateur> m_observateurs;
}

class Cabine implements IObservateur
{
	public void notifier()
	{
		System.out.println("Cabine a recu la notif");
		// Changement d'étage selon son sens et sa position précédente.
	}
}

class Moteur implements IObservateur
{
	public void notifier()
	{
		 System.out.println("Moteur a recu la notif");
		 // Verification que l'étage soit dans les bornes autorisées.
	}
}

class CapteurLaser extends Observable
{
   // Le code de la boucle while en environnement Threadé
	public void run()
	{
		while(true)
		{
			if(m_detecteVariation)
				notifierObservateurs();
		}
	}

	private boolean m_detecteVariation;
}


public class Run {

	/**
	 * @param args
	 */
	public static void main(String[] args) 
	{
		Cabine instanceCabine = new Cabine();
		Moteur instanceMoteur = new Moteur();
		
		CapteurLaser capteurEtage = new CapteurLaser();;
		
		capteurEtage.ajouterObservateur(instanceCabine);
		capteurEtage.ajouterObservateur(instanceMoteur);
		
		// On simule manuellement une variation (normalement c'est le thread qui s'en charge)
		capteurEtage.notifierObservateurs();
		
		// La cabine et le moteur ont reçu une notification sur leur méthode notifier()
		
		capteurEtage.supprimerObservateur(instanceMoteur);
		System.out.println("Suppression du moteur dans les abonnes");
		
		capteurEtage.notifierObservateurs();
	}
}

La sortie obtenue après exécution :

Cabine a recu la notif
Moteur a recu la notif
Suppression du moteur dans les abonnes
Cabine a recu la notif

Le principal dans tout ce que vous avez lu jusqu'à présent est le cheminement suivi. Si vous avez assimilé cette démarche c'est que l'essentiel sur ce chapitre vous a été retransmis. Il est très important de de suivre un raisonnement et de réfléchir avant de partir tête baissée dans le code. J'insiste encore sur le fait qu'une bonne conception est la clef de voute pour du code réutilisable, extensible et robuste. Je rappelle que mon objectif premier sur ce cours n'est pas de vous apprendre à utiliser les design patterns mais d'apprendre à concevoir d'une meilleure manière. Il existe tellement de sources sur internet qui traitent des patrons de conception que je n'ai aucune prétention de faire mieux. Je développe plutôt la démarche qui montre le résultat d'une conception très réfléchie à partir d'idées concrètes.

Maintenant sachez qu'en Java vous ne serez pas obligés de devoir écrire le code de l'Observateur et de l'Observable, ce design pattern étant tellement utilisé, il existe dans l'API de base :

// Exemple tiré de wikipédia
class Signal extends Observable {
 
  void setData(byte[] lbData){
    setChanged(); // Positionne son indicateur de changement
    notifyObservers(); // (1) notification
  }
}
/*
    On crée le panneau d'affichage qui implémente l'interface java.util.Observer.
    Avec une méthode d'initialisation (2), on lui transmet le signal à observer (2).
    Lorsque le signal notifie une mise à jour, le panneau est redessiné (3).*/

class JPanelSignal extends JPanel implements Observer {
 
  void init(Signal lSigAObserver) {
    lSigAObserver.addObserver(this); // (2) ajout d'observateur
  }
 
  void update(Observable observable, Object objectConcerne) {
    repaint();  // (3) traitement de l'observation
  }
}

Le code varie un peu, on peut voir que la méthode update(...) correspond à notre méthode notifier().

Mais attend ! Pourquoi il y a deux arguments supplémentaires dans la méthode o_O ?

Pas de panique, un petit tour dans la documentation nous éclaire très vite. Le premier argument correspond à l'objet émetteur de la notification, alors que le second paramètre permet de passer des valeurs relatives à cet évènement. Prenons pour exemple un thermomètre qui lorsqu'il change sa température va envoyer la notification à ses abonnés. Oui mais on veut aussi obtenir quelle température était indiquée au thermomètre lorsqu'il a émis la notification. C'est le rôle du second argument. Le premier argument quant à lui permet de reconnaitre qui a indiqué la notification si notre objet s'est abonné à plusieurs sources (par exemple un objet qui reçoit la notification de deux thermomètres différents).

Connaître l’émetteur peut vous sembler superflu mais dans la programmation événementielle ce cas arrive assez souvent. Pour ceux qui auraient un doute, la programmation événementielle s'utilise très souvent lorsque les logiciels fonctionnent avec une interface graphique. Tout est évènement sur les interfaces et regardez sur une fenêtre à l'apparence simple le nombre d'évènements que l'on peut lancer :

IHM

Il n'y a que la moitié des éléments (encadrés en rouge) pouvant lancer des notifications sur mon exemple.
Sachez que chaque bouton, liste déroulante ou autre utilise en principe une base de ce design pattern (même si certains IDE camouflent leur utilisation à l'aide d'outils graphiques pour dessiner les fenêtres). C'est un patron de conception qui a un très fort taux d'utilisation.

Pour bien insister sur le fonctionnement de ce patron de conception, je vais vous proposer un exemple dans une application graphique. Il sera ici question d'une interface de panier et vous verrez que son utilisation est moins intuitive qu'elle en a l'air. Mais penchons nous sur le sujet : Une liste d'articles est représentée et affichée dans un contrôle avec un montant total n'est pas calculé à l'affichage. Un bouton permet d’estimer le coût total des composants et de le renseigner dans un champ prévu à cet effet. L'image qui suit devrait vous faciliter la compréhension :

IHM observer à placer.

IHM d'un panier. On peut voir le bouton qui permet de calculer le prix et de remplir le champ "Total".
Le problème avec le bouton, c'est qu'aucune capture d'évènement n'est associée dessus. Un bouton est à la base un simple composant comme le reste, inerte. Il est nécessaire de venir lui indiquer qu'un clic utilisateur va engendrer un comportement spécifique. C'est là que le pattern observateur entre en jeu : l'utilisateur est donc observé pour savoir si une pression sur le bouton a été exercée, tandis que le bouton devient un observateur. Autant vous le dire tout de suite, les programmes avec interfaces graphiques camouflent la partie d'utilisateur observé (le framework ou le système d'exploitation de charge de cet élément). Vous, en tant que programmeur, vous n'avez qu'à vous préoccuper du bouton et de le rendre observateur. La méthode pour rendre un composant graphique observateur diffère selon le langage et les outils utilisés mais le principe reste toujours le même. Le résultat sera donc le suivant :

IHM bouton écoute clic utilisateur.

Le bouton devient observateur d'un clic. Lorsque l'utilisateur appuiera sur le bouton, l'interface graphique va appeler l'évènement que le bouton écoute.

C'est ainsi que s'achève notre première étude de cas. J'espère que ce chapitre aura été enrichissant pour vous. si tel est le cas, j'aurai réussi mon pari. Assurez vous de comprendre comment fonctionne ce patron de conception car il est utilisé dans de nombreux cas et même dans d'autres patrons de conception plus complexes.

Si vous n'avez pas saisi quelque chose, relisez ce chapitre à tête.

Example of certificate of achievement
Example of certificate of achievement