Langage de programmation:
Design pattern: 

Implémentation du pattern Observateur en Java et utilisation de l’API correspondante. L’exemple présenté est le positionnement par GPS.

Description du problème

Afin d’illustrer l’implémentation du pattern Observateur en Java réalisons une petite application permettant de se positionner grâce au GPS. Le principe du Global Positioning System est simple. Une personne souhaitant connaître sa position utilise un récepteur GPS. Ce récepteur reçoit des informations (position, date précise…) d’au moins quatre satellites (sur un total de 24 satellites). Grâce à la date transmise, le récepteur peut calculer la distance le séparant du satellite dont il connaît la position. Il renouvelle l’opération avec trois autres satellites et peut donc en déduire sa position dans l’espace (procédé appelé là trilatération). Si vous désirez en savoir plus sur le GPS vous pouvez consulter l’article de Wikipédia sur ce sujet.

Considérons que notre ordinateur est relié à un récepteur GPS par un réseau sans fil. On va concevoir une classe nommée Gps qui va stocker les informations du récepteur (positionnement, précision…). Puis deux autres classes (AfficheResume et AfficheComplet) permettant d’afficher de deux façons différentes ces informations. Comme dans la définition du pattern Observateur, on trouve également deux interfaces Observateur et Observable. Pour résumer la classe Gps sera observable et les classes AfficheResume et AfficheComplet seront ses observateurs. Voyons plus en détails le diagramme UML et l’implémentation de cette application.


Implémentation d’une solution personnelle basée sur Observateur

Diagramme UML de l'implémentation en Java du design pattern Observateur

Sur ce diagramme UML on reconnaît aisément le pattern Observateur. La classe Gps est l’observable et possède deux observateurs potentiels que sont AfficheResume et AfficheComplet. Commençons par regarder la définition des deux interfaces.


Définition des interfaces Observateur et Observable.

// Interface implémentée par tous les observateurs.
public interface Observateur
{
        // Méthode appelée automatiquement lorsque l'état (position ou précision) du GPS change.
        public void actualiser(Observable o);
}
// Interface implémentée par toutes les classes souhaitant avoir des observateurs à leur écoute.
public interface Observable
{
        // Méthode permettant d'ajouter (abonner) un observateur.
        public void ajouterObservateur(Observateur o);
        // Méthode permettant de supprimer (résilier) un observateur.
        public void supprimerObservateur(Observateur o);
        // Méthode qui permet d'avertir tous les observateurs lors d'un changement d'état.
        public void notifierObservateurs();
}


L’interface Observateur impose aux observateurs d’implémenter la méthode actualiser(Observable). Celle-ci sera appelée automatiquement lors d’un changement d’état dans la classe observable. L’interface Observable oblige les classes souhaitant disposer d’observateurs d’implémenter trois méthodes. Les méthodes ajouterObservateur(Observateur) et supprimerObservateur(Observateur) permettent, respectivement, d’ajouter (abonner) et de supprimer (désabonner) un observateur. La méthode notifierObservateurs() se charge d’avertir tous les observateurs abonnés d’un changement d’état.


Implémentation de la classe Gps

// Classe représentant un GPS (appareil permettant de connaître sa position).
public class Gps implements Observable
{
        private String position;// Position du GPS.
        private int precision;// Précision accordé à cette position (suivant le nombre de satellites utilisés).
        private ArrayList tabObservateur;// Tableau d'observateurs.
       
        // Constructeur.
        public Gps()
        {
                position="inconnue";
                precision=0;
                tabObservateur=new ArrayList();
        }
       
        // Permet d'ajouter (abonner) un observateur à l'écoute du GPS.
        public void ajouterObservateur(Observateur o)
        {
                tabObservateur.add(o); 
        }
       
        // Permet de supprimer (résilier) un observateur écoutant le GPS
        public void supprimerObservateur(Observateur o)
        {
                tabObservateur.remove(o);              
        }

        // Méthode permettant de notifier tous les observateurs lors d'un changement d'état du GPS.
        public void notifierObservateurs()
        {
                for(int i=0;i<tabObservateur.size();i++)
                {
                        Observateur o = tabObservateur.get(i);
                        o.actualiser(this);// On utilise la méthode "tiré".
                }
        }

        // Méthode qui permet de mettre à jour de façon artificielle le GPS.
        // Dans un cas réel, on utiliserait les valeurs retournées par les capteurs.
        public void setMesures(String position, int precision)
        {
                this.position=position;
                this.precision=precision;
                notifierObservateurs();
        }
       
        // Méthode "tiré" donc c'est aux observeurs d'aller chercher les valeurs désiré grâce à un objet Gps.
        // Pour cela on trouve un accesseur en lecture pour position.
        public String getPosition()
        {
                return position;
        }
        // Un accesseur en lecture pour précision.
        public int getPrecision()
        {
                return precision;
        }
}


La classe Gps implémente l’interface Observable et peut ainsi avoir des observateurs à son écoute. La liste des observateurs à l’écoute est stockée dans un tableau dynamique en attribut. De plus, cette classe possède deux états un correspondant au positionnement et l’autre à la précision. La précision indiquera la fiabilité du positionnement (plus on utilise de satellites plus la précision est forte). Un constructeur permet d’initialiser les attributs. Les trois méthodes définies dans l’interface Observable sont implémentées. De plus, on trouve deux accesseur en lecture correspondant aux deux états de la classe. En effet, nous utiliserons la méthode « TIRER » comme présentée dans la description du pattern Observateur. Enfin la méthode setMesures(String, int) permettra de simuler la réception de valeurs par le récepteur GPS. En effet, nous n’allons pas vous obliger à acheter un récepteur GPS pour tester l’application…


Implémentation des classes AfficheResume et AfficheComplet

// Affiche un résumé en console des informations (position) du GPS.
public class AfficheResume implements Observateur
{
        // Méthode appelée automatiquement lors d'un changement d'état du GPS.
        public void actualiser(Observable o)
        {
                if(o instanceof Gps)
                {       
                        Gps g = (Gps) o;
                        System.out.println("Position : "+g.getPosition());
                }       
        }

}
// Affiche en console de façon complète les informations (position et précision) du GPS.
public class AfficheComplet implements Observateur
{
        // Méthode appelée automatiquement lors d'un changement d'état du GPS.
        public void actualiser(Observable o)
        {
                if(o instanceof Gps)
                {       
                        Gps g = (Gps) o;
                        System.out.println("Position : "+g.getPosition()+"  Précision : "+g.getPrecision()+"/10");
                }                     
        }

}


Ces deux classes implémentent la méthode actualiser(Observable) qui pour rappel est appelée automatiquement lors de changement d’états de la classe observable. Dans cette méthode on vérifie que l’observable est bien de type Gps et on utilise les accesseurs pour aller chercher le ou les états. La seule différence entre les deux classes concerne l’affichage en console de seulement la position pour AfficheResume et d’avantage d’informations (position et précision) pour AfficheComplet.


Implémentation de la classe Main

// Classe principale du projet.
public class Main
{
        // Méthode principale.
        public static void main(String[] args)
        {
                // Création de l'objet Gps observable.
                Gps g = new Gps();
                // Création de deux observeurs AfficheResume et AfficheComplet
                AfficheResume ar = new AfficheResume();
                AfficheComplet ac = new AfficheComplet();
                // On ajoute AfficheResume comme observeur de Gps.
                g.ajouterObservateur(ar);
                // On simule l'arrivée de nouvelles valeurs via des capteurs.
                g.setMesures("N 39°59°993 / W 123°00°000", 4);
                // On ajoute AfficheComplet comme observeur de Gps.
                g.ajouterObservateur(ac);
                // Nouvelle simulation d'arrivée de nouvelles valeurs via des capteurs.
                g.setMesures("N 37°48°898 / W 124°12°011", 5);
               
        }

}


Comme dans les autres exemples d’implémentations de patterns la classe Main correspond à la classe principale de notre programme. On commence par créer les objets suivants : Gps, AfficheResume et AfficheComplet. Puis on abonne les observateurs à l’objet de type Gps et on fait évoluer régulièrement les mesures.


Résultat en console

Position : N 39°59°993 / W 123°00°000
Position : N 37°48°898 / W 124°12°011
Position : N 37°48°898 / W 124°12°011  Précision : 5/10


Une fois l’observateur AfficheResume abonné à l’observable Gps il est tenu au courant des changements d’état. Ce n’est pas le cas de l’objet de type AfficheComplet car même s’il implémente l’interface Observateur l’objet n’est pas ajouté à la liste des observateurs de Gps. Une fois cette opération effectuée on constate que les deux observateurs sont bien tenus informés des changements d’état de Gps.


Implémentation d’une solution basée sur l’API Java

Diagramme UML de l'implémentation en Java (API) du design pattern Observateur

Après une implémentation personnelle du pattern Observateur en Java voyons ce que propose l’API. Celle-ci facilite l’utilisation de ce pattern en fournissant une classe Observable et une interface Observer. L’interface Observer équivaut à notre interface Observateur la méthode appelée automatiquement lors de changement d’état étant update(Observable, Object). On remarque que la signature de cette méthode permet, aux choix, de « TIRER » ou « POUSSER ». La classe Observable diffère un peu de notre interface Observable précédente. Tout d’abord il s’agit d’une classe ce qui est inconvénient notable, cependant cela permet d’y insérer une partie de l’implémentation. Voyons les modifications que cela engendre dans le modèle et l’implémentation de notre exemple.


Implémentation de la classe Gps

//Classe représentant un GPS (appareil permettant de connaître sa position).
public class Gps extends Observable
{
        private String position;// Position du GPS.
        private int precision;// Précision accordé à cette position (suivant le nombre de satellite utilisés).
       
        // Constructeur.
        public Gps()
        {
                position="inconnue";
                precision=0;
        }
       
        // Méthode permettant de notifier tous les observateurs lors d'un changement d'état du GPS.
        public void notifierObservateurs()
        {
                setChanged();// Méthode de l'API.
                notifyObservers();// Egalement une méthode de l'API.
        }

        // Méthode qui permet de mettre à jour de façon artificielle le GPS.
        // Dans un cas réel, on utiliserait les valeurs retournées par les capteurs.
        public void setMesures(String position, int precision)
        {
                this.position=position;
                this.precision=precision;
                notifierObservateurs();
        }
       
        // Méthode "tiré" donc c'est aux observeurs d'aller chercher les valeurs désiré grâce à un objet Gps.
        // Pour cela on trouve un accesseur en lecture pour position.
        public String getPosition()
        {
                return position;
        }
        // Un accesseur en lecture pour précision.
        public int getPrecision()
        {
                return precision;
        }
}


Le code de la classe Gps est bien plus léger en utilisant l’API. On retrouve, le constructeur, les deux accesseurs en lecture pour les états, la méthode setMesures(String, int) et la méthode notifierObservateurs(). Cependant l’implémentation de cette dernière a été modifiée puisqu’elle appelle seulement deux méthodes de l’API : setChanged() et notifyObservers(). La méthode setChanged() permet de mettre à vrai un attribut de Observable. Et c’est seulement si cette attribut est à vrai que notifyObservers() informera les observateurs de changement (puis modifiera cet attribut à faux). L’intérêt de ce procéder est de contrôler les appels aux observateurs afin d’avoir la possibilité de ne pas les appeler pour un changement d’état insignifiant (position déplacée d’un millimètre par exemple).


Implémentation des classes AfficheResume et AfficheComplet

//Affiche un résumé en console des informations (position) du GPS.
public class AfficheResume implements Observer
{
        // Méthode appelée automatiquement lors d'un changement d'état du GPS.
        public void update(Observable o, Object obj)
        {
                if(o instanceof Gps)
                {       
                        Gps g = (Gps) o;
                        System.out.println("Position : "+g.getPosition());
                }       
        }

}
//Affiche en console de façon complète les informations (position et précision) du GPS.
public class AfficheComplet implements Observer
{
        // Méthode appelée automatiquement lors d'un changement d'état du GPS.
        public void update(Observable o, Object obj)
        {
                if(o instanceof Gps)
                {       
                        Gps g = (Gps) o;
                        System.out.println("Position : "+g.getPosition()+"  Précision : "+g.getPrecision()+"/10");
                }                     
        }

}


Le code est très similaire à l’exemple précédent. Seul le nom et la signature de la méthode a été modifiée. Comme énoncé précédemment cette nouvelle signature met en place les deux modes de récupération de l’état « TIRER » ou « POUSSER ».


Implémentation de la classe Main

//Classe principale du projet observeurApi.
public class Main
{
        // Méthode principale.
        public static void main(String[] args)
        {
                // Création de l'objet Gps observable.
                Gps g = new Gps();
                // Création de deux observeurs AfficheResume et AfficheComplet
                AfficheResume ar = new AfficheResume();
                AfficheComplet ac = new AfficheComplet();
                // On ajoute AfficheResume comme observeur de Gps.
                g.addObserver(ar);
                // On simule l'arrivée de nouvelles valeurs via des capteurs.
                g.setMesures("N 39°59°993 / W 123°00°000", 4);
                // On ajoute AfficheComplet comme observeur de Gps.
                g.addObserver(ac);
                // Nouvelle simulation d'arrivée de nouvelles valeurs via des capteurs.
                g.setMesures("N 37°48°898 / W 124°12°011", 5);
        }
}


Seul le nom des méthodes appelées est modifié dans la classe Main. On garde volontairement le même exemple.


Résultat en console

Position : N 39°59°993 / W 123°00°000
Position : N 37°48°898 / W 124°12°011  Précision : 5/10
Position : N 37°48°898 / W 124°12°011


On constate que le résultat est le même que précédemment. Ce qui est logique puisque nous avons pris le même exemple et nous avons utilisé dans les deux cas le pattern Observateur.


Conclusion

Maintenant que nous savons implémenter notre propre pattern Observateur et utiliser celui de l’API nous pouvons nous demander lequel est-il préférable d’utiliser ? La réponse est : ça dépend des cas. Si on souhaite une implémentation rapide on peut utiliser l’API Java. En revanche, dans certain cas, on sera obligé de faire une implémentation personnelle du pattern Observateur. En effet, dans l’API, Observable est une classe. Il faut donc que la classe qui souhaite gérer des observateurs puisse hériter de celle-ci. Or l’héritage multiple n’existe pas en Java, donc ceci n’est possible que si cette classe n’hérite pas déjà d’une autre classe. On comprend alors l’utilité de définir une interface plutôt qu’une classe.

Pour résumer l’implémentation du pattern Observateur est souvent utilisé et peu complexe à mettre en œuvre. Il fait donc partie des patterns indispensables à connaître, que l’on rencontre régulièrement lors de développement.

Code source: 
Commentaires

Bonjour,

Merci pour ce tuto, au début j’avais du mal avec GPS (poutant GPS est fait pour ne pas se perdre !!!) mais après ça allait ;-)

Une petite question : quelle est l’utilité du paramètre obj dans la méthode update ?

javadoc dit : an argument passed to the notifyObservers method

Pourra-tu l’expliquer un peu s’il te plaît et si possible un exemple concret (si ce n’est pas trop demandé)

Merci par avance

passionnéeDuJava

Bonjour Mathieu,

Merci de ta réponse, tu expliques tellement bien que j’espère trouver encore d’autres tuto sur ton site ;-)

Finalement est-ce qu’on peut dire :

s’il y a 

notifyObservers(Object quoi)

on aura

public void update (Observable qui, Object quoi)

Une dernière petite question : est-ce que le mot observable ici est une lapsus ou c’est moi qui n’ai pas compris :

En fait lors de l’utilisation de la méthode update() d’Observable ...

bonne journée

passionnéeDuJava

Bonjour Mathieu,

C’est toujours la passionnéeDeJava qui pose une nouvelles petite question ;-)

Pour la version où nous créons une interface observable (à notre goût), si j’ai bien compris on se fait aider par la classe Observable du java et on crée nos méthodes (de notre interface) comme les méthodes de la classe java.

Donc notre interface ne dépends aucune interface ni une classe (elle ne fait ni extends ni implements).

et justement est-ce qu’on peut créer une interface et la faire héritée de la classe Observable pour avoir toutes ses possibilités ??

ex :

monInterface extends Observable .......

Bonsoir, je ne sais pas si je déterre le sujet (posté il y a 3ans) mais j’aurais une petite question sur ce design pattern.

j’explique d’abord mon contexte ca sera plus facile à comprendre.

mon programme gère l’arbitrage d’une course à pied, je releve des flashs de coureurs qui passent sur des points de passage ; quand un point est franchi je crée un Evenement, pour cela j’ai créé une classe evenement (générique) et plusieurs classes d’événements qui héritent de celle-ci (par exemple : evtDepart, evtArrivée ...).

je "pousse" mes événements aux observeurs donc on a la méthode update (Evenement e) et je voudrais que les observeurs puissent choisir quels types d’événement ils traitent. pour cela j’ai surchargé la méthode update dans les classes observeurs.

donc on se retrouve dans l’interface avec update ( Evenement e ) et dans les classe qui l’implémente update (evtDepart ), ... Seulement je ne veut faire qu’une seule méthode notifieObserveurs() dans le sujet donc j’utilise l’événement générique dans cette méthode, et les observeurs voit arriver un objet Evenement et ne font plus de traitement spécialisé...

le seul moyen que j’ai trouvé est de faire une methode notifieObserveurs() pour chaque type d’événement a traiter dans le sujet,

y aurait-il un moyen de garder le bénéfice de la surcharge de update en envoyant un evenement générique (qui ne l’est pas en fin de compte) ?

Merci de votre reponse, j’ai deja plusieur méthodes notifierObserveur(), mais justement je n’en voulais qu’une. Je pensais que la redifinition marcherait....

J’ai un diagramme de cette partie de l’architecture. je n’ai pas les sources pour le moment car le projet et finit (et je l’ai rendu avc plusieurs notifierObserveurs() ). Mais cette question me tracasse.... (ou je suis fou de faire du dev pendant les vacances =D ). Je vous envoie ce que j’ai et j’essairais de récuperrer les sources si besoin est.

merci encore de votre réponse.

Arnaud

 

Le format de ce cours est exceptionnel. Comme beaucoup, j'attends la suite de tes articles sur les dp avec impatience.

Bon courage et merci

 

Possum4D

Ajouter un commentaire