Le design pattern Observer

Comment synchroniser plusieurs vues sur une donnée unique ?

Guillaume Scheibel

- Titre : Le design pattern Observer

- Auteur : Guillaume Scheibel

- Parution : 14 avril 2010

- Mise à jour : 14 avril 2010

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2010 Guillaume Scheibel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- 0. Introduction

- I. Pré requis

- II. Le pattern

- II-A. L'observé

- II-B. Les observateurs

- II-B-1. Le TableModel

- II-B-2. La JFrame

- II-C. Le lien Observateurs / Observé

- II-C-1. Le ThreadManagerActionListener

- II-C-2. Le ShowViewFrameListener

- III. Conclusion

0 - Introduction

Souvent lors du développement d'une interface graphique, il est nécessaire de coordonner plusieurs vues (composants) sur une même donnée. Lorsque la donnée change, il faut donc rafraichir l'ensemble des vues en question afin qu'il n'y ait pas de problèmes. Cependant, comment éviter la multiplication des liens entre la donnée et les vues ?

I - Pré requis

Afin de ne pas avoir de difficultés de compréhension, il est nécessaire de connaître les bases du développement en Java (notions sur les listeners, Swing et les threads).

II - Le pattern

Le pattern Observer fait partie des patterns dit « de Responsabilité » car il permet de mieux gérer le « qui fait quoi ?». De plus, lors de la phase de conception il est plus que recommandé d'anticiper les possibles évolutions, dans notre cas cela pourrait être l'ajout d'une vue qui va « regarder » notre donnée. Si l'on se réfère au pattern architectural MVC (Model-View-Controller), qu'il est conseillé d'appliquer afin d'avoir un développement évolutif, nous obtenons trois couches. Le modèle qui constituera la donnée à observer, les vues qui utiliseront la donnée et le contrôleur qui fera le lien entre les deux couches précédentes.

II-A - L'observé

Afin d'avoir une mise à jour régulière des données, nous utiliserons un Thread. Ce thread incrémentera une valeur, dans notre cas un entier, puis s'endormira pour un temps donné.

Nous allons donc créer une classe contenant un entier nommé count :

public class CounterJob {
private int count;
public CounterJob() {
super();
}

public int getCount() {
return count;
}

public void setCount(int count) {
this.count = count;
}
}

Cet objet étant l'élément sur lequel vont se synchroniser les vues il doit donc pouvoir être observé. Pour cela, notre classe doit hériter de la classe java.util.Observable. Nous obtenons donc la signature suivante :

public class CounterJob extends Observable

Comme nous l'avons dit précédemment, les changements sur notre donnée seront effectués grâce à un thread, puisque la classe hérite déjà d'une classe nous implémentons alors l'interface Runnable (du package java.lang).

public class CounterJob extends Observable implements Runnable

L'interface Runnable nous oblige à implémenter la méthode run(). Dans cette méthode nous allons incrémenter la valeur de l'attribut count puis bloquer l'exécution du thread durant 1,5 seconde.

public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
this.setCount(this.getCount() + 1);
Thread.sleep(1500);
}
} catch (InterruptedException e) {
}
}

Le principe du pattern Observer impose à la donnée observée de signaler à tous ses observateurs qu'un changement est survenu. Pour cela, nous allons utiliser deux méthodes : setChanged() et notifyObservers(Object o) toutes deux contenues dans la classe Observer. Afin de ne pas oublier de faire ces deux étapes à chaque modification de l'attribut count, nous allons donc modifier le setter de la façon suivante :

public void setCount(int count) {
this.count = count;
this.setChanged();
this.notifyObservers(this.getCount());
}

L'utilisation de la méthode setChanged() est obligatoire car lorsque les observateurs vont vérifier si un changement est intervenu, ils feront appel à la méthode hasChanged(). Si hasChanged() renvoie false, ils n'appliqueront pas les mises à jour que vous avez faites. Enfin, il est possible de d'avertir un observateur spécifique (méthode notifyObserver(Observer obs) ou de tous les prévenir, chose que nous faisons ici.

Il existe deux écritures de la méthode notifyObserver(). La première ne prend aucun paramètre et se contente d'avertir l'observateur qu'une modification a été faite. La seconde écriture prend un paramètre de type Object. Cette écriture nous permettra d'envoyer la donnée modifiée. L'avantage à ce niveau est que l'observateur n'est pas fortement dépendant de l'observé (seul le type de la donnée observée, ici un entier, les lie).

II-B - Les observateurs

Le gros avantage dans l'utilisation du pattern Observer est que l'on peut créer une multitude d'observateurs différents. Ils n'auront aucun lien entre eux et pourront avoir des tâches complètements différentes. Pour illustrer ce point, nous allons créer deux observateurs, le premier un TableModel (que nous utiliserons dans un JTable) et le second sera une fenêtre (héritage sur JFrame). Afin de créer un observateur, il faut obligatoirement implémenter l'interface java.util.Observer. Chaque observateur doit définir ses propres actions lorsqu'une modification survient. Cette définition se fera dans l'implémentation de la méthode upate(Observable obs, Object obj).

II-B-1 - Le TableModel

Le table model que nous allons créer est une spécialisation du DefaultTableModel. Il va contenir une liste de String ainsi qu'un DateFormat qui sera utilisée pour composer les données affichées dans la table (une date suivie de la donnée Observable). Voici la signature de la classe :

public class ObsTableModel extends DefaultTableModel implements Observer

Comme nous l'avons dit précédemment, chaque observateur doit redéfinir la méthode upate(). Pour ce tableModel, lorsqu'une modification survient, nous allons mettre à jour la liste des données et rafraîchir la table.

@Override
public void update(Observable obs, Object obj) {
Date date = new Date();
this.getValues().add(new String(this.getFormat().format(date)+ " " + obj));
this.fireTableDataChanged();
}

Dans cette méthode, nous récupérons la nouvelle valeur grâce au paramètre obj (ce paramètre est transmit par l'observé lorsqu'il appelle notifyObservers(Object o) ).

Notre table (contenue dans la fenêtre principale de l'application) ressemblera à :

II-B-2 - La JFrame

Le second observateur sera une spécialisation de JFrame contenant un label (JLabel) dont le texte sera mit à jour à chaque appelle de la méthode update().

Voici la signature de la classe :

public class ViewFrame extends JFrame implements Observer

Ainsi que la méthode update :

@Override
public void update(Observable o, Object obj) {
this.getLabel().setText("Value="+obj);
}

Voici la fenêtre secondaire :

II-C - Le lien Observateurs / Observé

Comme vous pouvez le voir sur les captures d'écran ci-dessus, notre MainFrame est composée de deux boutons. Le premier nous permet de lancer et stopper le thread de compte et le second affichera la fenêtre secondaire.

II-C-1 - Le ThreadManagerActionListener

Cette classe implémente l'interface java.awt.event.ActionListener et sera associée au bouton permettant de lancer le Thread et donc l'incrémentation de notre donnée. Dans cette classe, il nous faut :

- Une référence à la fenêtre principale afin de mettre à jour le texte du bouton (« Start Thread » et « Stop thread »)

- Un Thread qui contiendra le CounterJob

- Le CounterJob car il nous faut bien la donnée observée.

Dans le getter du Thread, s'il n'existe pas alors on le recrée en lui passant le counterJob :

public Thread getThread() {
if (this.thread == null) {
this.setThread(new Thread(this.getCounterJob()));
}
return thread;
}

Le getter du CounterJob quant à lui va instancier l'objet counterJob et faire les liens avec les observateurs :

public CounterJob getCounterJob() {
if (this.counterJob == null) {
this.counterJob = new CounterJob();
this.counterJob.addObserver(this.getMainFrame().getObsTableModel()
this.counterJob.addObserver(this.getMainFrame().getViewFrame());
}
return this.counterJob;
}

La liaison Observater / Observable se fait en utilisant la méthode addObserver() de l'Observable.

Enfin, la méthode actionPerformed :

@Override
public void actionPerformed(ActionEvent arg0) {
if (this.getThread().isAlive()) {
this.getThread().interrupt();
this.getMainFrame().getBtnStart().setText("Start Thread");
this.setThread(null);
} else {
this.getMainFrame().getBtnStart().setText("Stop Thread");
this.getThread().start();
}
}

II-C-2 - Le ShowViewFrameListener

Cette classe gère le fait d'afficher ou de cacher la fenêtre secondaire. L'instance de cette fenêtre secondaire est passée en paramètre au constructeur de la classe. Voici la méthode actionPerformed :

@Override
public void actionPerformed(ActionEvent ae) {
if(this.getViewFrame().isVisible()){
if(ae.getSource() instanceof JButton){
JButton btn = (JButton) ae.getSource();
btn.setText("Show ViewFrame");
}
} else {
if(ae.getSource() instanceof JButton){
JButton btn = (JButton) ae.getSource();
btn.setText("Hide ViewFrame");
}
}
this.getViewFrame().setVisible(!this.getViewFrame().isVisible());
}

III - Conclusion

Nous avons donc créé plusieurs vues dont les affichages sont synchronisés sur une donnée. Cette synchronisation est faite sans qu'il y ait de lien direct entre la couche de vue et celle de donnée (couche Model).

Grâce à ce pattern, nous gagnons en souplesse et en modularité car chaque partie ne maîtrise que ce qui lui est propre (les vues s'occupent de mettre à jour l'affichage et le model met à jour la donnée « critique » puis signale aux observateurs qu'il y eu modification).