Una vista interna di Observer

Non molto tempo fa la mia frizione ha ceduto, quindi ho fatto rimorchiare la mia jeep in una concessionaria locale. Non conoscevo nessuno alla concessionaria e nessuno di loro mi conosceva, quindi ho dato loro il mio numero di telefono in modo che potessero avvisarmi con un preventivo. Quell'arrangiamento ha funzionato così bene che abbiamo fatto la stessa cosa quando il lavoro è stato completato. Poiché tutto questo si è rivelato perfettamente per me, sospetto che il reparto di assistenza presso la concessionaria utilizzi lo stesso modello con la maggior parte dei suoi clienti.

Questo modello di pubblicazione-sottoscrizione, in cui un osservatore si registra con un oggetto e successivamente riceve le notifiche , è abbastanza comune, sia nella vita di tutti i giorni che nel mondo virtuale dello sviluppo del software. In effetti, il pattern Observer , come è noto, è uno dei cardini dello sviluppo di software orientato agli oggetti perché consente a oggetti dissimili di comunicare. Questa capacità consente di collegare oggetti in un framework in fase di esecuzione, il che consente un software altamente flessibile, estensibile e riutilizzabile.

Nota: puoi scaricare il codice sorgente di questo articolo da Risorse.

Il modello Observer

In Design Patterns , gli autori descrivono il pattern Observer in questo modo:

Definire una dipendenza uno a molti tra gli oggetti in modo che quando un oggetto cambia stato, tutti i suoi dipendenti vengono notificati e aggiornati automaticamente.

Il modello Observer ha un soggetto e potenzialmente molti osservatori. Gli osservatori si registrano con il soggetto, che avvisa gli osservatori quando si verificano eventi. L'esempio prototipo di Observer è un'interfaccia utente grafica (GUI) che visualizza simultaneamente due viste di un singolo modello; le viste si registrano con il modello e quando il modello cambia, notifica le viste, che si aggiornano di conseguenza. Vediamo come funziona.

Osservatori in azione

L'applicazione mostrata nella Figura 1 contiene un modello e due viste. Il valore del modello, che rappresenta l'ingrandimento dell'immagine, viene manipolato spostando la manopola di scorrimento. Le viste, note come componenti in Swing, sono un'etichetta che mostra il valore del modello e un riquadro di scorrimento che ridimensiona un'immagine in base al valore del modello.

Il modello nell'applicazione è un'istanza di DefaultBoundedRangeModel(), che tiene traccia di un valore intero limitato, in questo caso da 0a, 100con questi metodi:

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

Come indicano gli ultimi due metodi sopra elencati, le istanze di DefaultBoundedRangeModel()supporto cambiano listener. L'esempio 1 mostra come l'applicazione sfrutta questa funzionalità:

Esempio 1. Due osservatori reagiscono ai cambiamenti del modello

import javax.swing. *; importa javax.swing.event. *; import java.awt. *; import java.awt.event. *; import java.util. *; public class Test estende JFrame { private DefaultBoundedRangeModel model = new DefaultBoundedRangeModel (100,0,0,100); slider JSlider privato = nuovo JSlider ( modello ); private JLabel readOut = nuova JLabel ("100%"); immagine ImageIcon privata = nuova ImageIcon ("shortcake.jpg"); ImageView privato imageView = nuovo ImageView (immagine, modello); public Test () {super ("The Observer Design Pattern"); Contenitore contentPane = getContentPane (); Pannello JPanel = nuovo JPanel (); panel.add (new JLabel ("Set Image Size:")); panel.add (slider); panel.add (readOut); contentPane.add (pannello, BorderLayout.NORTH); contentPane.add (imageView, BorderLayout.CENTER);model.addChangeListener (new ReadOutSynchronizer ()); } public static void main (String args []) {Test test = new Test (); test.setBounds (100,100,400,350); test.show (); } la classe ReadOutSynchronizer implementa ChangeListener {public void stateChanged (ChangeEvent e) {String s = Integer.toString (model.getValue ()); readOut.setText (s + "%"); readOut.revalidate (); }}} la classe ImageView estende JScrollPane {pannello JPanel privato = nuovo JPanel (); dimensione privata originalSize = nuova dimensione (); immagine privata originalImage; icona ImageIcon privata; public ImageView (icona ImageIcon, modello BoundedRangeModel) {panel.setLayout (new BorderLayout ()); panel.add (nuova JLabel (icona)); this.icon = icon; this.originalImage = icon.getImage (); setViewportView (pannello);model.addChangeListener (new ModelListener ()); originalSize.width = icon.getIconWidth (); originalSize.height = icon.getIconHeight (); } la classe ModelListener implementa 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 (); doppio moltiplicatore = (doppio) valore / (doppio) span; moltiplicatore = moltiplicatore == 0,0? 0,01: moltiplicatore; Immagine scalata = originalImage.getScaledInstance ((int) (originalSize.width * moltiplicatore), (int) (originalSize.height * moltiplicatore), Image.SCALE_FAST); icon.setImage (scalato); panel.revalidate (); panel.repaint (); }}}}

Quando sposti la manopola del cursore, il cursore cambia il valore del suo modello. Questa modifica attiva le notifiche degli eventi ai due listener di modifiche registrati con il modello, che regolano la lettura e ridimensionano l'immagine. Entrambi i listener utilizzano l'evento di modifica passato a

stateChanged()

per determinare il nuovo valore del modello.

Swing è un utente pesante del pattern Observer: implementa più di 50 listener di eventi per implementare il comportamento specifico dell'applicazione, dalla reazione a un pulsante premuto al veto di un evento di chiusura della finestra per un frame interno. Ma Swing non è l'unico framework che fa buon uso del pattern Observer: è ampiamente utilizzato in Java 2 SDK; ad esempio: Abstract Window Toolkit, il framework JavaBeans, il javax.namingpacchetto e gestori di input / output.

L'esempio 1 mostra specificamente l'uso del pattern Observer con Swing. Prima di discutere ulteriori dettagli del pattern Observer, diamo un'occhiata a come il pattern è generalmente implementato.

Come funziona il pattern Observer

La Figura 2 mostra come sono correlati gli oggetti nel pattern Observer.

Il soggetto, che è una sorgente di eventi, mantiene una raccolta di osservatori e fornisce metodi per aggiungere e rimuovere osservatori da quella raccolta. Il soggetto implementa anche un notify()metodo che notifica a ciascun osservatore registrato gli eventi che interessano l'osservatore. I soggetti informano gli osservatori invocando il update()metodo dell'osservatore .

La Figura 3 mostra un diagramma di sequenza per il pattern Observer.

In genere, un oggetto non correlato richiama il metodo di un soggetto che modifica lo stato del soggetto. Quando ciò accade, il soggetto invoca il proprio notify()metodo, che itera sulla raccolta di osservatori, chiamando il update()metodo di ciascun osservatore .

Il pattern Observer è uno dei design pattern più fondamentali perché consente la comunicazione di oggetti altamente disaccoppiati. Nell'esempio 1, l'unica cosa che il modello a intervallo limitato sa dei suoi ascoltatori è che implementano un stateChanged()metodo. Gli ascoltatori sono interessati solo al valore del modello, non a come il modello viene implementato. Il modello e i suoi ascoltatori sanno molto poco l'uno dell'altro, ma grazie al pattern Observer possono comunicare. Questo alto grado di disaccoppiamento tra modelli e listener consente di creare software composto da oggetti collegabili, rendendo il codice altamente flessibile e riutilizzabile.

Java 2 SDK e il pattern Observer

Java 2 SDK fornisce un'implementazione classica del pattern Observer con l' Observerinterfaccia e la Observableclasse dalla java.utildirectory. La Observableclasse rappresenta il soggetto; gli osservatori implementano l' Observerinterfaccia. È interessante notare che questa classica implementazione del pattern Observer è usata raramente nella pratica perché richiede ai soggetti di estendere la Observableclasse. Richiedere l'ereditarietà in questo caso è una cattiva progettazione perché potenzialmente qualsiasi tipo di oggetto è un soggetto candidato e perché Java non supporta l'ereditarietà multipla; spesso, quei candidati soggetti hanno già una superclasse.

The event-based implementation of the Observer pattern, which was used in the preceding example, is the overwhelming choice for Observer pattern implementation because it doesn't require subjects to extend a particular class. Instead, subjects follow a convention that requires the following public listener registration methods:

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

Whenever a subject's bound property (a property that's been observed by listeners) changes, the subject iterates over its listeners and invokes the method defined by the XXXListener interface.

By now you should have a good grasp of the Observer pattern. The rest of this article focuses on some of the Observer pattern's finer points.

Anonymous inner classes

In Example 1, I used inner classes to implement the application's listeners, because the listener classes were tightly coupled to their enclosing class; however, you can implement listeners any way you desire. One of the most popular choices for handling user interface events is the anonymous inner class, which is a class with no name that's created in-line, as demonstrated in Example 2:

Example 2. Implement observers with anonymous inner classes

... 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'uso di classi interne anonime come mostrato nell'esempio precedente era molto popolare tra gli sviluppatori, quindi a partire da Java 2 Platform, Standard Edition (J2SE) 1.4, la specifica JavaBeans si è presa la responsabilità di implementare e istanziare quelle classi interne per te con la EventHandlerclasse, come mostrato nell'esempio 3:

Esempio 3. Utilizzo di java.beans.EventHandler

import java.beans.EventHandler; ... public class Test estende 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 (); }} ...