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).