Semaine 11
Activité 2
Patron de conception Observer
Le patron de conception Observer permet de réduire le couplage pouvant exister entre une composante logicielle étant une source d’événements et les composantes logicielles devant être notifiées de ces événements. Ce patron de conception met en place deux classes principales :
- la classe Observer : les instances de ces classes sont celles intéressées par les événements;
- la classe Source : les instances de ces classes sont celles générant des événements.
Un exemple permet d’illustrer facilement le fonctionnement du patron Observer. Supposons qu’il existe un système informatique fonctionnant par message texte (SMS) permettant d’être notifié des résultats de notre équipe sportive préférée à la fin de chacune de ses parties. Ce système simple fonctionne ainsi : l’utilisateur à partir de son téléphone mobile (une instance d’un Observer) envoie un message texte quelconque (même vide) au numéro du système informatique (l’instance de la Source). Celui-ci stocke le numéro du téléphone mobile de l’utilisateur dans une structure de données. À la fin de chaque partie de l’équipe, le système itère au travers de la structure de données, envoyant un message texte contenant le résultat de la partie à chacun des numéros de téléphone mobile stockés. Le système peut être étendu à plusieurs équipes sportives ayant chacun leur numéro spécifique et un utilisateur peut être inscrit à plusieurs équipes sportives. Dans la vie courante, de tels systèmes existent et Twitter fonctionne un peu selon la même mécanique.
L’idée principale est d’implémenter un tel système avec des classes permettant, d’une part, à un Observer d’être enregistré à une ou à plusieurs sources d’événements et, d’autre part, à une source de notifier plusieurs Observers. À ces fonctions s’ajoute la capacité de retirer un Observer d’une Source. Une telle architecture permet beaucoup de versatilité dans le développement logiciel et réduit considérablement le couplage pouvant exister entre des composantes logicielles. Le patron de conception Observer est entre autres l’une des façons de relier la Vue et le Contrôleur dans l’approche MVC, tout en réduisant le couplage entre les composantes des deux groupes.
Voici le diagramme UML (tiré de wikipédia – domaine public) représentant la structure des classes et interfaces du patron. On peut utiliser la domination Source ou Subject (comme dans le diagramme suivant) :
Voici le code générique Java pour implémenter ces composantes.
Interface Observer
Cette interface spécifie la méthode permettant aux classes qui implémenteront la méthode d’être notifiées des événements. L’événement ici est représenté par un Object; il est possible dans une implémentation spécifique de le remplacer par un autre type d’objet.
public interface Observer { public void notify(Object event); }
Classe Subject
Les trois méthodes de base de la classe Subject permettent d’ajouter des observateurs, d’en enlever et de notifier ceux-ci lors d’un événement.
public class Subject { ArrayList<Observer> observers = new ArrayList<Observer>(); public synchronized void addObserver(Observer o) { observers.add(o); } public synchronized void removeObserver(Observer o) { observers.remove(o); } protected void notifyObservers(Object event) { for(Observer observer : observers) { observer.notify(event); } } }
Exemple d’implémentation d’un observateur
public class ExempleObservateur implements Observer { @Override public void notify(Object event) { //Traitement quelconque ... System.out.println("Event =" + event); } }
Exemple du patron avec une IHM
Reprenons l’exercice pratique sur le MVC de la liste d’employés. Dans cet exemple, le couplage entre la Vue et le Contrôleur était fort, c’est-à-dire qu’il y avait une relation directe entre le Contrôleur et la Vue. Advenant une modification du Contrôleur ou l’ajout d’autres Contrôleurs associés à la Vue, on doit modifier le code de la Vue en conséquence. Or, en utilisant le patron Observer, il est possible d’éliminer ce couplage au maximum. Voici donc la façon de procéder par étapes et le résultat final.
Partie 1
1 – Créer l’interface AjoutEmployeObserver :
public interface AjoutEmployeObserver { public void notifyAjoutEmploye(Employe e); }
2- Ajouter l’implémentation de l’interface AjoutEmployeObserver à la classe ControleurEmploye
public class ControleurEmploye implements AjoutEmployeObserver { ArrayList<Employe> employes = new ArrayList<Employe>(); AjoutEmployeJFrame ajoutJFrame; ListingEmployesJFrame listingJFrame; public ControleurEmploye() { listingJFrame = new ListingEmployesJFrame(this); ajoutJFrame = new AjoutEmployeJFrame(); ajoutJFrame.addAjoutEmployeObserver(this); listingJFrame.setVisible(true); ajoutJFrame.setVisible(true); } /** * @param args the command line arguments */ public static void main(String[] args) { ControleurEmploye controleur = new ControleurEmploye(); } public void ajouterEmploye(Employe employe) { // Ajout de l'employé employes.add(employe); // Mise ? jour du listing des employes listingJFrame.mettreAJourListing(); } public ArrayList<Employe> getListEmployee() { return employes; } @Override public void notifyAjoutEmploye(Employe e) { this.ajouterEmploye(e); } }
3- Implémenter les fonctionnalités de la classe Subject dans la classe AjoutEmployeJFrame. Du coup, il est possible d’enlever les références vers le Contrôleur dans cette classe et, par conséquent, d’éliminer le couplage.
public class AjoutEmployeJFrame extends javax.swing.JFrame { ArrayList<AjoutEmployeObserver> observers = new ArrayList<AjoutEmployeObserver>(); /** * Creates new form AjoutEmployeJFrame */ public AjoutEmployeJFrame() { initComponents(); } public synchronized void addAjoutEmployeObserver(AjoutEmployeObserver observer) { observers.add(observer); } public synchronized void removeAjoutEmployeObserver(AjoutEmployeObserver observer) { observers.remove(observer); } public synchronized void notifyAjouterEmployeObservers(Employe e) { for(AjoutEmployeObserver o : observers) { o.notifyAjoutEmploye(e); } } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ @SuppressWarnings("unchecked") // <editor-fold defaultstate="collapsed" desc="Generated Code"> private void initComponents() { jLabel2 = new javax.swing.JLabel(); jLabel1 = new javax.swing.JLabel(); jPanel1 = new javax.swing.JPanel(); jButton1 = new javax.swing.JButton(); jPanel2 = new javax.swing.JPanel(); jLabel3 = new javax.swing.JLabel(); jLabel4 = new javax.swing.JLabel(); jLabel5 = new javax.swing.JLabel(); jLabel6 = new javax.swing.JLabel(); jTextField1 = new javax.swing.JTextField(); jTextField2 = new javax.swing.JTextField(); jTextField3 = new javax.swing.JTextField(); jTextField4 = new javax.swing.JTextField(); jLabel2.setText("jLabel2"); jLabel1.setText("jLabel1"); setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); jButton1.setText("Ajout"); jButton1.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { jButton1ActionPerformed(evt); } }); javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); jPanel1.setLayout(jPanel1Layout); jPanel1Layout.setHorizontalGroup( jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() .addGap(143, 143, 143) .addComponent(jButton1) .addContainerGap(179, Short.MAX_VALUE)) ); jPanel1Layout.setVerticalGroup( jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() .addGap(33, 33, 33) .addComponent(jButton1) .addContainerGap(38, Short.MAX_VALUE)) ); getContentPane().add(jPanel1, java.awt.BorderLayout.PAGE_END); jPanel2.setBorder(javax.swing.BorderFactory.createTitledBorder("Info")); jLabel3.setText("Nom"); jLabel4.setText("Metier"); jLabel5.setText("Salaire"); jLabel6.setText("Année d'emploi"); javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2); jPanel2.setLayout(jPanel2Layout); jPanel2Layout.setHorizontalGroup( jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel2Layout.createSequentialGroup() .addGap(53, 53, 53) .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(jLabel3) .addComponent(jLabel4) .addComponent(jLabel5) .addComponent(jLabel6)) .addGap(40, 40, 40) .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) .addComponent(jTextField1, javax.swing.GroupLayout.DEFAULT_SIZE, 102, Short.MAX_VALUE) .addComponent(jTextField2) .addComponent(jTextField3) .addComponent(jTextField4)) .addContainerGap(96, Short.MAX_VALUE)) ); jPanel2Layout.setVerticalGroup( jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel2Layout.createSequentialGroup() .addGap(10, 10, 10) .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel3) .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGap(18, 18, 18) .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel4) .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGap(18, 18, 18) .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel5) .addComponent(jTextField3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGap(18, 18, 18) .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel6) .addComponent(jTextField4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); getContentPane().add(jPanel2, java.awt.BorderLayout.CENTER); pack(); }// </editor-fold> private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) { Employe employe = new Employe(); employe.setNom(jTextField1.getText()); employe.setMetier(jTextField2.getText()); employe.setSalaire(Integer.parseInt(jTextField3.getText())); employe.setAnneeEmbauche(Integer.parseInt(jTextField4.getText())); notifyAjouterEmployeObservers(employe); } // Variables declaration - do not modify private javax.swing.JButton jButton1; private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel2; private javax.swing.JLabel jLabel3; private javax.swing.JLabel jLabel4; private javax.swing.JLabel jLabel5; private javax.swing.JLabel jLabel6; private javax.swing.JPanel jPanel1; private javax.swing.JPanel jPanel2; private javax.swing.JTextField jTextField1; private javax.swing.JTextField jTextField2; private javax.swing.JTextField jTextField3; private javax.swing.JTextField jTextField4; // End of variables declaration }
Dans cette solution, le couplage de la Vue vers le Contrôleur est nul, c’est-à-dire que la classe AjoutEmployeJFrame n’a pas de référence directe vers le Contrôleur à l’exception de son abstraction via l’interface AjoutEmployeObserver. De plus, cette solution permet d’ajouter d’autres classes susceptibles de recevoir des notifications de l’ajout d’un employé. Ainsi, il serait possible d’utiliser le patron Observer pour éliminer également la ligne « listingJFrame.mettreAJourListing(); » de la classe ControleurEmploye en notifiant l’instance de la classe ListingEmployesJFrame directement par l’intermédiaire d’une implémentation de l’interface AjoutEmployeObserver dans cette « Vue ». Toutefois, une telle fonctionnalité viendrait briser le paradigme MVC où c’est le contrôleur qui notifie les Vues de modifications possibles et non une autre « Vue ». Voici donc la solution pour réduire encore là le couplage entre le Contrôleur et la classe ListingEmployesJFrame.
Partie 2
4 – Créer une nouvelle interface MiseAJourListingObserver :
import java.util.ArrayList; public interface MiseAJourListingObserver { public void notifyMiseAJourListing(ArrayList<Employe> employes); }
5- Implémenter l’interface MiseAJourListingObserver dans la classe ListingEmployesJFrame. Ceci permet d’éliminer les références vers le Contrôleur et la méthode miseAJourListing
public class ListingEmployesJFrame extends javax.swing.JFrame implements MiseAJourListingObserver { ArrayList<Employe> employes = new ArrayList<Employe>(); /** * Creates new form ListingEmployesJFrame */ public ListingEmployesJFrame() { initComponents(); AbstractTableModel model = new AbstractTableModel() { @Override public String getColumnName(int column) { if(column == 0) { return "Nom"; } else if(column == 1) { return "Metier"; } else if(column == 2) { return "Salaire"; } else { return "Nombre d'année d'embauche"; } } @Override public int getRowCount() { return employes.size(); } @Override public int getColumnCount() { return 4; } @Override public Object getValueAt(int rowIndex, int columnIndex) { if(columnIndex == 0) { return employes.get(rowIndex).getNom(); } else if(columnIndex == 1) { return employes.get(rowIndex).getMetier(); } else if(columnIndex == 2) { return employes.get(rowIndex).getSalaire(); } else { return employes.get(rowIndex).getAnneeEmbauche(); } } }; jTable1.setModel(model); } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ @SuppressWarnings("unchecked") // <editor-fold defaultstate="collapsed" desc="Generated Code"> private void initComponents() { jScrollPane1 = new javax.swing.JScrollPane(); jTable1 = new javax.swing.JTable(); setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); jTable1.setModel(new javax.swing.table.DefaultTableModel( new Object [][] { {null, null, null, null}, {null, null, null, null}, {null, null, null, null}, {null, null, null, null} }, new String [] { "Nom", "Metier", "Salaire", "Nombre d'année d'embauche" } ) { boolean[] canEdit = new boolean [] { false, false, false, false }; public boolean isCellEditable(int rowIndex, int columnIndex) { return canEdit [columnIndex]; } }); jScrollPane1.setViewportView(jTable1); getContentPane().add(jScrollPane1, java.awt.BorderLayout.CENTER); pack(); }// </editor-fold> // Variables declaration - do not modify private javax.swing.JScrollPane jScrollPane1; private javax.swing.JTable jTable1; // End of variables declaration @Override public void notifyMiseAJourListing(ArrayList<Employe> employes) { this.employes = employes; jTable1.updateUI(); } }
6 – Ajouter les méthodes de la classe Subject au Contrôleur.
public class ControleurEmploye implements AjoutEmployeObserver { ArrayList<Employe> employes = new ArrayList<Employe>(); ArrayList<MiseAJourListingObserver> observers = new ArrayList<MiseAJourListingObserver>(); AjoutEmployeJFrame ajoutJFrame; ListingEmployesJFrame listingJFrame; public ControleurEmploye() { listingJFrame = new ListingEmployesJFrame(); ajoutJFrame = new AjoutEmployeJFrame(); ajoutJFrame.addAjoutEmployeObserver(this); listingJFrame.setVisible(true); ajoutJFrame.setVisible(true); } public synchronized void addMiseAJourListingObserver(MiseAJourListingObserver o) { observers.add(o); } public synchronized void removeMiseAJourListingObserver(MiseAJourListingObserver o) { observers.remove(o); } public synchronized void notifyMiseAJourListingObservers(ArrayList<Employe> employes) { for(MiseAJourListingObserver o : observers) { o.notifyMiseAJourListing(employes); } } /** * @param args the command line arguments */ public static void main(String[] args) { ControleurEmploye controleur = new ControleurEmploye(); } public void ajouterEmploye(Employe employe) { // Ajout de l'employé employes.add(employe); notifyMiseAJourListingObservers(employes); } public ArrayList<Employe> getListEmployee() { return employes; } @Override public void notifyAjoutEmploye(Employe e) { this.ajouterEmploye(e); } }
Exemple du patron avec les ActionListener de l’API Swing/AWT
L’API Swing et AWT incluent le patron de conception Observer dans sa gestion des événements sur les composantes graphiques. Par exemple, comme nous l’avons vu durant les semaines précédentes, pour être informé de l’utilisation d’un bouton « JButton », il est nécessaire d’enregistrer un observateur implémentant l’interface ActionListener au bouton afin de recevoir les événements. Ceux-ci sont décrits par l’objet ActionEvent, les informations sur la source de l’événement et, s’il y a lieu, le type d’événement. Par exemple, l’action d’appuyer sur le bouton « Fermer » de la composante JFileChooser générera un événement avec un identifiant différent de celui du bouton « Ouvrir ».
L’API Swing intègre également la gestion des événements avec l’utilisation de périphériques d’interaction tels que la souris et le clavier. Pour ce faire, il est possible de créer des instances des classes « MouseAdapter » ou « KeyAdapter » pour recevoir des événements lorsque le curseur de la souris survole une composante graphique ou lorsqu’on appuie sur une touche du clavier alors que la composante graphique est sélectionnée.
Pour illustrer l’utilisation des ActionListener, supposons un programme comportant deux classes JFrame : l’une comportant un bouton « Appuyer »; l’autre comportant des composantes affichant le nombre de clics sur le bouton précédent et l’heure à laquelle on a appuyé sur le bouton la dernière fois. Afin d’informer le second JFrame des événements, nous implémenterons l’interface ActionListener dans le second JFrame, afin de recevoir les événements.
Note : Pour les besoins de l’exemple, je vais à l’encontre des bonnes pratiques de programmation et du paradigme MVC. À comprendre, mais à ne pas faire!
1- JFrame des informations sur les clics
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.ZoneId; import java.util.Date; import java.util.TimeZone; public class AffichageJFrame extends javax.swing.JFrame implements ActionListener { private int nbClick = 0; /** * Creates new form AffichageJFrame */ public AffichageJFrame() { initComponents(); } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ @SuppressWarnings("unchecked") // <editor-fold defaultstate="collapsed" desc="Generated Code"> private void initComponents() { jLabel1 = new javax.swing.JLabel(); jLabel2 = new javax.swing.JLabel(); jTextField1 = new javax.swing.JTextField(); jTextField2 = new javax.swing.JTextField(); setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); jLabel1.setText("Nombre de click :"); jLabel2.setText("Dernier click :"); jTextField1.setEditable(false); jTextField2.setEditable(false); jTextField2.setEnabled(false); jTextField2.setFocusable(false); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); getContentPane().setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addGap(100, 100, 100) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(jLabel1) .addComponent(jLabel2)) .addGap(31, 31, 31) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) .addComponent(jTextField2, javax.swing.GroupLayout.DEFAULT_SIZE, 121, Short.MAX_VALUE) .addComponent(jTextField1)) .addContainerGap(67, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addGap(97, 97, 97) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel1) .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGap(18, 18, 18) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel2) .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addContainerGap(145, Short.MAX_VALUE)) ); pack(); }// </editor-fold> // Variables declaration - do not modify private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel2; private javax.swing.JTextField jTextField1; private javax.swing.JTextField jTextField2; // End of variables declaration @Override public void actionPerformed(ActionEvent e) { nbClick++; jTextField1.setText(Integer.toString(nbClick)); // Convertir la date en format de type "nombre de millisecondes depuis le 1 Jan 1970". Date date = new Date(e.getWhen()); // Afficher dans le bon format et dans la bonne zone DateFormat formatter = new SimpleDateFormat("dd MMM yyyy HH:mm:ss z"); formatter.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); jTextField2.setText(formatter.format(date)); } }
2 – JFrame du bouton « Appuyer »
import java.awt.event.ActionListener; public class BoutonJFrame extends javax.swing.JFrame { /** * Creates new form BoutonJFrame */ public BoutonJFrame(ActionListener listener) { initComponents(); jButton1.addActionListener(listener); } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ @SuppressWarnings("unchecked") // <editor-fold defaultstate="collapsed" desc="Generated Code"> private void initComponents() { jButton1 = new javax.swing.JButton(); setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); jButton1.setText("Appuyer"); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); getContentPane().setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addGap(151, 151, 151) .addComponent(jButton1) .addContainerGap(176, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addGap(126, 126, 126) .addComponent(jButton1) .addContainerGap(151, Short.MAX_VALUE)) ); pack(); }// </editor-fold> // Variables declaration - do not modify private javax.swing.JButton jButton1; // End of variables declaration }
3- Classe Main
public class ObserverExemple { public static void main(String[] args) { AffichageJFrame affichageJFrame = new AffichageJFrame(); BoutonJFrame boutonJFrame = new BoutonJFrame(affichageJFrame); affichageJFrame.setVisible(true); boutonJFrame.setVisible(true); } }
