Aller au menu - Aller au contenu

Soyez à l'écoute de vos objets : le pattern observer


Informations sur le tutoriel

Avatar
Auteur : cysboy
Difficulté : Intermédiaire
Visualisations : 331 817
Licence : Creative Commons BY-NC-SA


Plus d'informations Plus d'informations

Historique des mises à jour

  • Aujourd'hui à 17:36:42
    Correction orthographique, ticket n°1826
  • Aujourd'hui à 17:35:03
    Corrections orthographiques, tickets n°1824 et n°1825
  • Le 19/02/2010 à 11:14:30
    Corrections orthographiques, tickets n°1680 et n°1681
Dans ce chapitre, nous verrons comment faire dialoguer vos objets entre eux.
Vous allez voir que ceci est assez facile, au final, mais l'approche n'est pas évidente au premier abord !

Ce pattern est celui utilisé pour gérer les événements sur vos IHM. C'est par le biais de ce dernier que vos composants peuvent faire des actions lorsque vous cliquez dessus, que vous le survolez...

Je vois que vous êtes impatients de voir ce dernier ! Donc, let's go les zéros ! :pirate:
Chapitre précédent Sommaire Chapitre suivant

Posons le problème

Sachant que vous êtes un développeur Java chevronné, un de vos amis proches vous demande si vous pourriez l'aider à faire une horloge digitale en Java.
Celui-ci a la gentillesse de vous fournir les classes à utiliser afin de permettre de faire son horloge.

Votre ami a l'air de s'y connaître car ce qu'il vous a fourni est bien structuré.

Package com.sdz.vue classe Fenetre.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.sdz.vue;
import java.awt.BorderLayout;
import java.awt.Font;

import javax.swing.JFrame;
import javax.swing.JLabel;

import com.sdz.model.Horloge;

public class Fenetre extends JFrame{

	private JLabel label = new JLabel();
	private Horloge horloge;
	
	public Fenetre(){
		/* On initialise notre JFrame  */
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setLocationRelativeTo(null);
		this.setResizable(false);
		this.setSize(200, 80);
		/* On initialise l'horloge  */
		this.horloge = new Horloge();
		/* On initialise notre JLabel  */
		Font police = new Font("DS-digital", Font.TYPE1_FONT, 30);
		this.label.setFont(police);
		this.label.setHorizontalAlignment(JLabel.CENTER);
		/* On ajoute le JLabel à notre JFrame */
		this.getContentPane().add(this.label, BorderLayout.CENTER);		
	}


	/* Méthode main pour lancer le programme */
	public static void main(String[] args){
		Fenetre fen = new Fenetre();
		fen.setVisible(true);
	}
	
}


Package com.sdz.model classe Horloge.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.sdz.model;

import java.util.Calendar;

public class Horloge extends Thread{
        //Objet calendrier pour récupérer l'heure courante.
	private Calendar cal;
	private String hour = "";
	
	public Horloge(){
		Thread t = new Thread(this);
		t.start();
	}
	
	public void run() {
		while(true){
                                    
                                    //On récupère l'instance d'un calendrier à chaque tour 
	                            //celui-ci va nous permettre de récupérer l'heure actuelle
			            this.cal = Calendar.getInstance();
                                    this.hour = 	/* Les heures */
					this.cal.get(Calendar.HOUR_OF_DAY) + " : " 
					+ 
					( 		/* Les minutes */
						this.cal.get(Calendar.MINUTE) < 10
						? "0" + this.cal.get(Calendar.MINUTE)
						: this.cal.get(Calendar.MINUTE)
					)
					+ " : " 
					+
					( 		/* Les secondes */
						(this.cal.get(Calendar.SECOND)< 10) 
						? "0"+this.cal.get(Calendar.SECOND) 
						: this.cal.get(Calendar.SECOND)
					);
                        try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

Si vous ne possédez pas la même police d'écriture que j'ai utilisée, prenez-en une autre : Arial ou Courrier par exemple... ;)

Le problème auquel il est confronté est simple : impossible de pouvoir faire communiquer l'horloge avec sa fenêtre...

Je ne vois pas où est le problème ! Il n'a qu'à faire passer son instance de JLabel dans son objet Horloge et le tour est joué !

En fait, votre ami, dans son infinie sagesse, souhaite ne pas faire dépendre son horloge de son interface graphique et, je le cite, juste au cas où il devrait passer d'une IHM swing à une IHM AWT.

C'est vrai que si on passe notre objet d'affichage dans notre horloge, si nous devons changer de type d'IHM, toutes les classes devront être modifiées ! Pas génial...
En fait, lorsque vous faites ceci, on dit que vous couplez des objets : vous rendez un ou plusieurs objets dépendants d'un ou plusieurs autres objets !

Entendez par là que vous ne pourrez plus utiliser l'(les) objet(s) couplé(s) indépendamment de l'objet (des objets) auquel il(s) est(sont) attaché(s) !

Le couplage entre objets est l'un des problèmes principaux concernant la ré-utilisabilité d'objets...
Dans notre cas, si vous voulez utiliser votre objet Horloge ailleurs, vous serez confrontés à pas mal de problèmes puisque cet objet ne s'affichera que dans un JLabel !

Bon, on a compris : le pattern observer va tous nous sauver !

Tout à fait ! :)
Celui-ci va permettre de faire communiquer des objets entre eux sans qu'ils se connaissent réellement !
Vous devez être curieux de voir comment faire... je vous propose donc, sans plus tarder, de vous le présenter.

Des objets qui parlent et qui écoutent : le pattern observer

Nous allons faire un point sur ce que vous savez de ce pattern pour le moment :
  • il permet de faire communiquer des objets entre eux ;
  • c'est un bon moyen d'éviter le couplage entre les objets.

Ce sont tout de même deux points cruciaux, mais il y a encore un point qui va vous plaire et que vous ne savez pas encore : tout se fera automatiquement !

Comment les choses vont se passer ? Faisons un point de ce que nous voulons pour notre horloge digitale : celle-ci doit pouvoir avertir notre objet servant à afficher l'heure lorsque celui-ci doit mettre à jour son affichage.
Ici, vu que les horloges du monde entier se mettent à jour toutes les secondes, la nôtre fera de même... ^^

La chose merveilleuse avec ce pattern, c'est que notre horloge ne se contentera pas de mettre un objet au courant que sa valeur a changé, elle pourra même mettre plusieurs observateurs au courant ! ! :waw:

En fait, pour schématiser au maximum, voyez la relation entre les objets implémentant le pattern observer comme un éditeur de journal et ses clients :

Image utilisateur


Avec ce schéma, vous pouvez en conclure que notre objet défini comme observable pourra avoir plusieurs objets qui l'observent. On dit que cet objet a une relation de un à plusieurs vers l'objet Observateur.
Avant de vous expliquer un peu plus le principe de ce pattern, voici le diagramme de classe de notre application :

Image utilisateur


Avec ce diagramme, vous pouvez vous apercevoir que ce ne sont pas nos instances d'Horloge ou de JLabel que nous allons passer, mais des implémentations d'interfaces !

En effet, vous savez maintenant que les classes implémentant une interface peuvent être appelées par le type de l'interface. Dans notre cas, notre classe Fenetre va implémenter l'interface Observateur, celle-ci pourra donc être considérée comme un type Observateur !
Vous constaterez aussi que dans la deuxième interface, celle dédiée à l'objet Horloge, nous avons trois méthodes :
  • une qui permet d'ajouter des observateurs, nous allons donc gérer une collection d'observateurs ;
  • une qui permet de retirer les observateurs ;
  • et enfin une qui permet de mettre à jour les observateurs !

Grâce à ceci, nos objets ne sont plus liés par leur type respectif, mais par leurs interfaces !
L'interface qui va apporter les méthodes de mise à jour, d'ajout observateur... va travailler avec des objets de type Observateur !

Ainsi le couplage ne se fait plus directement, mais il s'opère par le biais de ces interfaces.
Tu nous as dit qu'il fallait éviter le couplage !

Oui, mais dans certains cas il est nécessaire... Ici, il faut que nos deux interfaces soient couplées pour que le système fonctionne. De même que, lors du chapitre précédent, nos classes étaient très fortement couplées puisqu'elles devaient travailler ensemble, nous devions donc faire en sorte de ne pas les séparer. ^^

Voici comment l'application va fonctionner.
  • Nous allons avoir une instance de la classe Horloge dans notre classe Fenetre.
  • Cette dernière va implémenter l'interface Observateur.
  • Notre objet Horloge, implémentant l'interface Observable, va attendre d'avoir des objets à prévenir de ses changements.
  • Nous allons informer notre horloge que notre fenêtre va l'observer.
  • À partir de là, notre objet Horloge va faire le reste : à chaque changement, nous allons appeler la méthode qui met à jour tous les observateurs ! :magicien:


Voici le code source de ces interfaces (j'ai créé un package com.sdz.observer).

Observateur.java



Code : Java
1
2
3
4
5
package com.sdz.observer;

public interface Observateur {
	public void update(String hour);
}


Observer.java



Code : Java
1
2
3
4
5
6
7
package com.sdz.observer;

public interface Observable {
	public void addObservateur(Observateur obs);
	public void updateObservateur();
	public void delObservateur();
}


Voici maintenant le code de nos deux classes, travaillant main dans la main mais ne se rencontrant JAMAIS.

Horloge.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.sdz.model;

import java.util.ArrayList;
import java.util.Calendar;

import com.sdz.observer.Observable;
import com.sdz.observer.Observateur;

public class Horloge extends Thread implements Observable{

	//On récupère l'instance d'un calendrier 
	//celui-ci va nous permettre de récupérer l'heure actuelle
	private Calendar cal;
	private String hour = "";
	//Notre collection d'observateurs !
	private ArrayList<Observateur> listObservateur = new ArrayList<Observateur>();
	
	public Horloge(){
		Thread t = new Thread(this);
		t.start();
	}
	
	public void run() {
		while(true){
			this.cal = Calendar.getInstance();
			this.hour = 	/* Les heures */
					this.cal.get(Calendar.HOUR_OF_DAY) + " : " 
					+ 
					( 		/* Les minutes */
						this.cal.get(Calendar.MINUTE) < 10
						? "0" + this.cal.get(Calendar.MINUTE)
						: this.cal.get(Calendar.MINUTE)
					)
					+ " : " 
					+
					( 		/* Les secondes */
						(this.cal.get(Calendar.SECOND)< 10) 
						? "0"+this.cal.get(Calendar.SECOND) 
						: this.cal.get(Calendar.SECOND)
					);
			
			//On avertit les observateurs que l'heure a été mise à jour !
			this.updateObservateur();
			
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	/**
	 * Ajoute un observateur à la liste
	 */
	public void addObservateur(Observateur obs) {
		this.listObservateur.add(obs);
	}
	/**
	 * Retire tous les observateurs de la liste
	 */
	public void delObservateur() {
		this.listObservateur = new ArrayList<Observateur>();
	}
	/**
	 * Avertit les observateurs que l'observable a changé 
	 * et invoque la méthode update de chaque observateur !
	 */
	public void updateObservateur() {
		for(Observateur obs : this.listObservateur )
			obs.update(this.hour);
	}
}



Fenetre.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.sdz.vue;
import java.awt.BorderLayout;
import java.awt.Font;

import javax.swing.JFrame;
import javax.swing.JLabel;

import com.sdz.model.Horloge;
import com.sdz.observer.Observateur;

public class Fenetre extends JFrame {

	private JLabel label = new JLabel();
	private Horloge horloge;
	
	public Fenetre(){
		/* On initialise notre JFrame  */
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setLocationRelativeTo(null);
		this.setResizable(false);
		this.setSize(200, 80);
		
		/* On initialise l'horloge  */
		this.horloge = new Horloge();
		//On place un écouteur sur notre horloge
		this.horloge.addObservateur(new Observateur(){
			public void update(String hour) {
				label.setText(hour);
			}
		});
		
		/* On initialise notre JLabel  */
		Font police = new Font("DS-digital", Font.TYPE1_FONT, 30);
		this.label.setFont(police);
		this.label.setHorizontalAlignment(JLabel.CENTER);
		/* On ajoute le JLabel à notre JFrame */
		this.getContentPane().add(this.label, BorderLayout.CENTER);		
	}


	/* Méthode main pour lancer le programme */
	public static void main(String[] args){
		Fenetre fen = new Fenetre();
		fen.setVisible(true);
	}
}


Exécutez ce code et vous verrez que tout fonctionne à merveille ! :magicien:
Vous venez de faire communiquer deux objets entre eux avec un couplage proche de zéro ! Félicitations !

Vous pouvez donc voir de vos yeux que lorsque l'heure change, la méthode updateObservateur() est invoquée. Celle-ci parcourt sa collection d'objets Observateur et appelle la méthode update(String hour) de celui-ci. La méthode étant redéfinie dans notre classe Fenetre afin de mettre à jour le JLabel, l'heure s'affiche !

La seule chose qui me dérange dans mon exemple tel qu'il est fait, c'est que vous ne voyez pas que l'objet observé met à jour tous les objets qui l'écoutent...
Afin de remédier à cela, nous allons quelque peu modifier notre objet Fenetre afin que celle-ci n'initialise pas tout de suite l'objet Horloge, et donc ne lui dise pas tout de suite que celui-ci est observé...

Pour ce faire, nous allons initialiser notre Horloge avec un mutateur dans la classe Fenetre et créer une autre classe, ZFenetre, qui elle, sera codée avec AWT.
Pour faire très simple, pour passer de swing à AWT, il vous suffit d'utiliser les mêmes objets en enlevant le "J" du début. Donc, JButton devient Button, JFrame devient Frame...


Voici les codes sources de ces deux classes.

ZFenetre.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.sdz.vue;

import java.awt.BorderLayout;
import java.awt.Font;

import java.awt.*;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;

import javax.swing.JLabel;

import com.sdz.model.Horloge;
import com.sdz.observer.Observateur;

public class ZFenetre extends Frame{
	private Label label = new Label();
	private Horloge horloge;
	
	public ZFenetre(){
		this.setLocationRelativeTo(null);
		this.setResizable(false);
		this.setSize(200, 80);
				
		/* On initialise notre JLabel  */
		Font police = new Font("DS-digital", Font.TYPE1_FONT, 30);
		this.label.setFont(police);
		/* On ajoute le JLabel à notre JFrame */
		this.add(this.label, BorderLayout.CENTER);		
	}
	
	public void setHorloge(Horloge horloge) {
		this.horloge = horloge;
		//On place un écouteur sur notre horloge
		this.horloge.addObservateur(new Observateur(){
			public void update(String hour) {
				label.setText(hour);
			}
		});
	}
	
}


Fenetre.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.sdz.vue;
import java.awt.BorderLayout;
import java.awt.Font;

import javax.swing.JFrame;
import javax.swing.JLabel;

import com.sdz.model.Horloge;
import com.sdz.observer.Observateur;

public class Fenetre extends JFrame {

	private JLabel label = new JLabel();
	private Horloge horloge;
	
	public Fenetre(){
		/* On initialise notre JFrame  */
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setLocationRelativeTo(null);
		this.setResizable(false);
		this.setSize(200, 80);
				
		/* On initialise notre JLabel  */
		Font police = new Font("DS-digital", Font.TYPE1_FONT, 30);
		this.label.setFont(police);
		this.label.setHorizontalAlignment(JLabel.CENTER);
		/* On ajoute le JLabel à notre JFrame */
		this.getContentPane().add(this.label, BorderLayout.CENTER);		
	}
	
	public void setHorloge(Horloge horloge) {
		this.horloge = horloge;
		//On place un écouteur sur notre horloge
		this.horloge.addObservateur(new Observateur(){
			public void update(String hour) {
				label.setText(hour);
			}
		});
	}

	/* Méthode main pour lancer le programme */
	public static void main(String[] args){
		//Notre horloge UNIQUE
		Horloge horloge = new Horloge();
		
		//Notre fenêtre héritant de JFrame
		Fenetre fen = new Fenetre();
		fen.setVisible(true);
		fen.setHorloge(horloge);
		
		ZFenetre zFen = new ZFenetre();
		zFen.setVisible(true);
		zFen.setHorloge(horloge);
	}
}


Les deux codes diffèrent légèrement, vous avez dû remarquer que certaines méthodes de notre première fenêtre avaient disparu dans la deuxième...


Ce qui nous donne :

Image utilisateur


À gauche, la fenêtre faite avec AWT et à droite, celle faite avec swing !

Voilà, vous venez d'apprendre à utiliser votre troisième pattern de conception. :)
Je ne vous ai pas caché que celui-ci sert dans la gestion des événements d'interfaces graphiques. Vous avez remarqué que la syntaxe est identique.
Par contre, je vous ai caché quelque chose : il existe des classes Java qui permettent d'utiliser le pattern observer sans que vous ayez à coder les interfaces !

le pattern observer : le retour

En fait, il existe une classe abstraite, Observable et une interface Observer dans les classes Java !

Celles-ci fonctionnent de manière quasiment identique à notre façon de faire. Seuls quelques légers détails diffèrent.
Personnellement, je préfère de loin utiliser un pattern observer "maison".

Pourquoi ça ?

Tout simplement parce que l'objet que vous souhaitez observer DOIT hériter de la classe Observable. Ceci a donc pour conséquence que votre classe ne pourra plus hériter d'une autre classe vu que Java ne gère pas l'héritage multiple !


Image utilisateur

Pour les zéros qui ne l'auraient pas compris, ce qui entoure les deux classes sur notre schéma est en fait la symbolique UML d'un package ! Dans ce cas, le package java.util .

Vous pouvez voir que celui-ci fonctionne quasiment de la même manière que celui que nous avons développé. Il y a un toutefois un changement dans la méthode update(Observable obs, Object obj) : la signature (les paramètres) de la méthode a changé !
Celle-ci prend deux paramètres :
  • un objet Observable ;
  • un Object : donnée supplémentaire que vous souhaitez faire passer !


Vous connaissez le principe de fonctionnement de ce pattern. Il vous est donc facile de comprendre ce schéma. Toutefois, je vous invite à faire vos propres recherches sur l'implémentation de ce pattern dans Java. Vous verrez qu'il y a des subtilités (rien de méchant... ^^ ).

Je ne vais pas détailler cette implémentation ici, ce n'est pas le but de ce tutoriel.
Je voulais seulement vous montrer un pattern de conception et vous en expliquer le fonctionnement. Vous avez compris, vous savez l'utiliser et le refaire, par conséquent une petite recherche sur les classes Observer et Observable ne devrait pas vous poser beaucoup de problèmes ! :-°
Personnellement, j'ai été obligé de refaire l'implémentation de ce pattern à chaque fois car mes objets héritaient déjà d'une autre classe...

Ce qu'il faut retenir

  • Le pattern observer permet d'avoir des objets faiblement couplés.
  • Ce pattern est basé sur une relation : de un à plusieurs.
  • Au lieu de coupler nos objets directement, on préfère coupler les interfaces qu'ils implémentent.
  • Grâce à ce pattern, nos objets restent indépendants les uns des autres !

Chapitre très intéressant, n'est-ce pas ?
Vous pouvez maintenant faire communiquer vos objets entre eux sans problème et façon très optimisée.
Prenez le temps de bien assimiler ce pattern car il pourra sans doute vous sortir de situations complexes...

De plus, ce pattern est la base d'un pattern très connu : le pattern MVC (modèle, vue, contrôleur).

D'ailleurs, il est temps de voir celui-ci ! :pirate:
Chapitre précédent Sommaire Chapitre suivant

Informations sur le tutoriel

Retour en haut Retour en haut

Créé : Le 21/06/2006 à 15:02:22
Modifié : Le 12/02/2010 à 11:20:29
Avancement : 100%

4 commentaires