Une vue intérieure d'Observer

Il n'y a pas longtemps, mon embrayage a cédé, alors j'ai fait remorquer ma Jeep chez un concessionnaire local. Je ne connaissais personne chez le concessionnaire, et aucun d'entre eux ne me connaissait, alors je leur ai donné mon numéro de téléphone pour qu'ils puissent me communiquer une estimation. Cet arrangement a si bien fonctionné que nous avons fait la même chose une fois le travail terminé. Parce que tout s'est bien passé pour moi, je soupçonne que le service après-vente du concessionnaire utilise le même modèle avec la plupart de ses clients.

Ce modèle de publication-abonnement, où un observateur s'enregistre avec un sujet et reçoit ensuite des notifications , est assez courant, à la fois dans la vie quotidienne et dans le monde virtuel du développement de logiciels. En fait, le modèle Observer , comme on l'appelle, est l'un des piliers du développement logiciel orienté objet car il permet à des objets différents de communiquer. Cette capacité vous permet de brancher des objets dans un framework au moment de l'exécution, ce qui permet un logiciel hautement flexible, extensible et réutilisable.

Remarque: vous pouvez télécharger le code source de cet article à partir de Resources.

Le modèle Observer

Dans Design Patterns , les auteurs décrivent le pattern Observer comme ceci:

Définissez une dépendance un à plusieurs entre les objets afin que lorsqu'un objet change d'état, tous ses dépendants soient notifiés et mis à jour automatiquement.

Le modèle Observateur a un sujet et potentiellement plusieurs observateurs. Les observateurs s'inscrivent auprès du sujet, qui avertit les observateurs lorsque des événements se produisent. L'exemple prototypique d'Observer est une interface utilisateur graphique (GUI) qui affiche simultanément deux vues d'un seul modèle; les vues s'inscrivent dans le modèle et lorsque le modèle change, il en informe les vues, qui se mettent à jour en conséquence. Voyons voir comment ça fonctionne.

Observateurs en action

L'application illustrée à la figure 1 contient un modèle et deux vues. La valeur du modèle, qui représente l'agrandissement de l'image, est manipulée en déplaçant le curseur. Les vues, appelées composants dans Swing, sont une étiquette qui indique la valeur du modèle et un volet de défilement qui met à l'échelle une image en fonction de la valeur du modèle.

Le modèle dans l'application est une instance de DefaultBoundedRangeModel(), qui suit une valeur entière bornée - dans ce cas de 0à 100- avec ces méthodes:

  • int getMaximum()
  • int getMinimum()
  • int getValue()
  • boolean getValueIsAdjusting()
  • int getExtent()
  • void setMaximum(int)
  • void setMinimum(int)
  • void setValue(int)
  • void setValueIsAdjusting(boolean)
  • void setExtent(int)
  • void setRangeProperties(int value, int extent, int min, int max, boolean adjusting)
  • void addChangeListener(ChangeListener)
  • void removeChangeListener(ChangeListener)

Comme les deux dernières méthodes répertoriées ci-dessus l'indiquent, les instances de DefaultBoundedRangeModel()support changent les écouteurs. L'exemple 1 montre comment l'application tire parti de cette fonctionnalité:

Exemple 1. Deux observateurs réagissent aux changements de modèle

import javax.swing. *; import javax.swing.event. *; import java.awt. *; import java.awt.event. *; import java.util. *; public class Le test étend JFrame { private DefaultBoundedRangeModel model = new DefaultBoundedRangeModel (100,0,0,100); curseur JSlider privé = nouveau JSlider ( modèle ); private JLabel readOut = nouveau JLabel ("100%"); image ImageIcon privée = new ImageIcon ("shortcake.jpg"); ImageView imageView privée = nouvelle ImageView (image, modèle); public Test () {super ("Le modèle de conception d'observateur"); Conteneur contentPane = getContentPane (); Panneau JPanel = nouveau JPanel (); panel.add (nouveau JLabel ("Définir la taille de l'image:")); panel.add (curseur); panel.add (readOut); contentPane.add (panneau, BorderLayout.NORTH); contentPane.add (imageView, BorderLayout.CENTER);model.addChangeListener (nouveau ReadOutSynchronizer ()); } public static void main (String args []) {Test test = new Test (); test.setBounds (100,100,400,350); test.show (); } la classe ReadOutSynchronizer implémente ChangeListener {public void stateChanged (ChangeEvent e) {String s = Integer.toString (model.getValue ()); readOut.setText (s + "%"); readOut.revalidate (); }}} classe ImageView étend JScrollPane {panneau JPanel privé = new JPanel (); Dimension privée originalSize = nouvelle dimension (); Image privée originalImage; icône ImageIcon privée; public ImageView (icône ImageIcon, modèle BoundedRangeModel) {panel.setLayout (new BorderLayout ()); panel.add (nouveau JLabel (icône)); this.icon = icône; this.originalImage = icon.getImage (); setViewportView (panneau);model.addChangeListener (nouveau ModelListener ()); originalSize.width = icon.getIconWidth (); originalSize.height = icon.getIconHeight (); } la classe ModelListener implémente ChangeListener {public void stateChanged (ChangeEvent e) {BoundedRangeModel model = (BoundedRangeModel) e.getSource () ; if (model.getValueIsAdjusting ()) {int min = model.getMinimum (), max = model.getMaximum (), span = max - min, value = model.getValue (); multiplicateur double = (double) valeur / (double) span; multiplicateur = multiplicateur == 0,0? 0,01: multiplicateur; Image mise à l'échelle = originalImage.getScaledInstance ((int) (originalSize.width * multiplicateur), (int) (originalSize.height * multiplier), Image.SCALE_FAST); icon.setImage (mise à l'échelle); panel.revalidate (); panel.repaint (); }}}}

Lorsque vous déplacez le bouton du curseur, le curseur change la valeur de son modèle. Ce changement déclenche des notifications d'événement aux deux écouteurs de changement enregistrés avec le modèle, qui ajustent la lecture et mettent l'image à l'échelle. Les deux écouteurs utilisent l'événement change transmis à

stateChanged()

pour déterminer la nouvelle valeur du modèle.

Swing est un grand utilisateur du modèle Observer - il implémente plus de 50 écouteurs d'événements pour implémenter un comportement spécifique à une application, de la réaction à un bouton enfoncé au veto d'un événement de fermeture de fenêtre pour un cadre interne. Mais Swing n'est pas le seul framework qui utilise à bon escient le pattern Observer - il est largement utilisé dans le SDK Java 2; par exemple: la boîte à outils de la fenêtre abstraite, le framework JavaBeans, le javax.namingpackage et les gestionnaires d'entrée / sortie.

L'exemple 1 montre spécifiquement l'utilisation du modèle Observer avec Swing. Avant de discuter davantage des détails du modèle Observer, voyons comment le modèle est généralement implémenté.

Fonctionnement du modèle Observer

La figure 2 montre comment les objets du modèle Observer sont liés.

Le sujet, qui est une source d'événements, gère une collection d'observateurs et fournit des méthodes pour ajouter et supprimer des observateurs de cette collection. Le sujet met également en œuvre une notify()méthode qui informe chaque observateur enregistré des événements qui l'intéressent. Les sujets notifient les observateurs en invoquant la update()méthode de l'observateur .

La figure 3 montre un diagramme de séquence pour le modèle Observer.

En règle générale, certains objets non liés invoqueront la méthode d'un sujet qui modifie l'état du sujet. Lorsque cela se produit, le sujet invoque sa propre notify()méthode, qui itère sur la collection d'observateurs, appelant la update()méthode de chaque observateur .

Le modèle Observer est l'un des modèles de conception les plus fondamentaux car il permet aux objets fortement découplés de communiquer. Dans l'exemple 1, la seule chose que le modèle de plage bornée sait de ses écouteurs est qu'ils implémentent une stateChanged()méthode. Les auditeurs ne s'intéressent qu'à la valeur du modèle, pas à la manière dont le modèle est implémenté. Le modèle et ses auditeurs se connaissent très peu, mais grâce au pattern Observer, ils peuvent communiquer. Ce haut degré de découplage entre les modèles et les écouteurs vous permet de créer un logiciel composé d'objets enfichables, ce qui rend votre code hautement flexible et réutilisable.

Le SDK Java 2 et le modèle Observer

Le SDK Java 2 fournit une implémentation classique du modèle Observer avec l' Observerinterface et la Observableclasse du java.utilrépertoire. La Observableclasse représente le sujet; les observateurs mettent en œuvre l' Observerinterface. Il est intéressant de noter que cette implémentation de modèle Observer classique est rarement utilisée dans la pratique car elle nécessite des sujets pour étendre la Observableclasse. Exiger l'héritage dans ce cas est une mauvaise conception car potentiellement tout type d'objet est un sujet candidat, et parce que Java ne prend pas en charge l'héritage multiple; souvent, ces candidats sujets ont déjà une superclasse.

L'implémentation basée sur les événements du modèle Observer, qui a été utilisée dans l'exemple précédent, est le choix écrasant pour l'implémentation du modèle Observer car elle ne nécessite pas de sujets pour étendre une classe particulière. Au lieu de cela, les sujets suivent une convention qui nécessite les méthodes d'inscription d'écouteur public suivantes:

  • void addXXXListener(XXXListener)
  • void removeXXXListener(XXXListener)

Chaque fois que la propriété liée d' un sujet (une propriété qui a été observée par les écouteurs) change, le sujet itère sur ses écouteurs et appelle la méthode définie par l' XXXListenerinterface.

À présent, vous devriez avoir une bonne compréhension du modèle Observer. Le reste de cet article se concentre sur certains des points les plus fins du modèle Observer.

Classes internes anonymes

Dans l'exemple 1, j'ai utilisé des classes internes pour implémenter les écouteurs de l'application, car les classes d'écouteur étaient étroitement couplées à leur classe englobante; cependant, vous pouvez implémenter des écouteurs comme vous le souhaitez. L'un des choix les plus populaires pour gérer les événements de l'interface utilisateur est la classe interne anonyme, qui est une classe sans nom créée en ligne, comme illustré dans l'exemple 2:

Exemple 2. Implémentation d'observateurs avec des classes internes anonymes

... public class Test extends JFrame { ... public Test() { ... model.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { String s = Integer.toString(model.getValue()); readOut.setText(s + "%"); readOut.revalidate(); } }); } ... } class ImageView extends JScrollPane { ... public ImageView(final ImageIcon icon, BoundedRangeModel model) { ... model.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { BoundedRangeModel model = (BoundedRangeModel)e.getSource(); if(model.getValueIsAdjusting()) { int min = model.getMinimum(), max = model.getMaximum(), span = max - min, value = model.getValue(); double multiplier = (double)value / (double)span; multiplier = multiplier == 0.0 ? 0.01 : multiplier; Image scaled = originalImage.getScaledInstance( (int)(originalSize.width * multiplier), (int)(originalSize.height * multiplier), Image.SCALE_FAST); icon.setImage(scaled); panel.revalidate(); } } }); } } 

Example 2's code is functionally equivalent to Example 1's code; however, the code above uses anonymous inner classes to define the class and create an instance in one fell swoop.

JavaBeans event handler

L'utilisation de classes internes anonymes comme indiqué dans l'exemple précédent était très populaire auprès des développeurs, donc à partir de Java 2 Platform, Standard Edition (J2SE) 1.4, la spécification JavaBeans a pris la responsabilité d'implémenter et d'instancier ces classes internes pour vous avec la EventHandlerclasse, comme indiqué dans l'exemple 3:

Exemple 3. Utilisation de java.beans.EventHandler

import java.beans.EventHandler; ... public class Test étend JFrame {... public Test () {... model.addChangeListener (EventHandler.create (ChangeListener.class, this, "updateReadout")); } ... public void updateReadout () {String s = Integer.toString (model.getValue ()); readOut.setText (s + "%"); readOut.revalidate (); }} ...