Eine Innenansicht von Observer

Vor nicht allzu langer Zeit gab meine Kupplung nach, also ließ ich meinen Jeep zu einem örtlichen Händler schleppen. Ich kannte niemanden im Autohaus, und keiner von ihnen kannte mich, also gab ich ihnen meine Telefonnummer, damit sie mich mit einem Kostenvoranschlag benachrichtigen konnten. Dieses Arrangement hat so gut funktioniert, dass wir das Gleiche getan haben, als die Arbeit beendet war. Da dies alles perfekt für mich ausgefallen ist, vermute ich, dass die Serviceabteilung des Autohauses bei den meisten Kunden das gleiche Muster anwendet.

Dieses Publish-Subscribe-Muster, bei dem sich ein Beobachter bei einem Subjekt registriert und anschließend Benachrichtigungen erhält , ist sowohl im Alltag als auch in der virtuellen Welt der Softwareentwicklung weit verbreitet. Tatsächlich ist das sogenannte Observer- Muster einer der Dreh- und Angelpunkte der objektorientierten Softwareentwicklung, da es die Kommunikation unterschiedlicher Objekte ermöglicht. Mit dieser Funktion können Sie Objekte zur Laufzeit in ein Framework einbinden, was eine hochflexible, erweiterbare und wiederverwendbare Software ermöglicht.

Hinweis: Sie können den Quellcode dieses Artikels von Resources herunterladen.

Das Beobachtermuster

In Design Patterns beschreiben die Autoren das Observer-Muster folgendermaßen:

Definieren Sie eine Eins-zu-Viele-Abhängigkeit zwischen Objekten, damit alle abhängigen Objekte automatisch benachrichtigt und aktualisiert werden, wenn ein Objekt seinen Status ändert.

Das Beobachtermuster hat ein Subjekt und möglicherweise viele Beobachter. Beobachter registrieren sich beim Subjekt, wodurch die Beobachter benachrichtigt werden, wenn Ereignisse eintreten. Das prototypische Observer-Beispiel ist eine grafische Benutzeroberfläche (GUI), die gleichzeitig zwei Ansichten eines einzelnen Modells anzeigt. Die Ansichten werden beim Modell registriert. Wenn sich das Modell ändert, werden die Ansichten benachrichtigt, die entsprechend aktualisiert werden. Mal sehen, wie es funktioniert.

Beobachter in Aktion

Die in Abbildung 1 gezeigte Anwendung enthält ein Modell und zwei Ansichten. Der Wert des Modells, der die Bildvergrößerung darstellt, wird durch Bewegen des Schiebereglers manipuliert. Die Ansichten, die in Swing als Komponenten bezeichnet werden, sind eine Beschriftung, die den Wert des Modells anzeigt, und ein Bildlaufbereich, der ein Bild entsprechend dem Wert des Modells skaliert.

Das Modell in der Anwendung ist eine Instanz von DefaultBoundedRangeModel(), die einen begrenzten ganzzahligen Wert - in diesem Fall von 0bis 100- mit den folgenden Methoden verfolgt:

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

Wie die letzten beiden oben aufgeführten Methoden zeigen, DefaultBoundedRangeModel()ändern Instanzen der Unterstützung die Listener. Beispiel 1 zeigt, wie die Anwendung diese Funktion nutzt:

Beispiel 1. Zwei Beobachter reagieren auf Modelländerungen

import javax.swing. *; import javax.swing.event. *; import java.awt. *; import java.awt.event. *; import java.util. *; public class Test erweitert JFrame { private DefaultBoundedRangeModel model = new DefaultBoundedRangeModel (100,0,0,100); privater JSlider-Schieberegler = neuer JSlider ( Modell ); private JLabel readOut = neues JLabel ("100%"); private ImageIcon image = neues ImageIcon ("shortcake.jpg"); private ImageView imageView = neue ImageView (Bild, Modell); public Test () {super ("The Observer Design Pattern"); Container contentPane = getContentPane (); JPanel-Panel = neues JPanel (); panel.add (neues JLabel ("Set Image Size:")); panel.add (Schieberegler); panel.add (readOut); contentPane.add (Panel, BorderLayout.NORTH); contentPane.add (imageView, BorderLayout.CENTER);model.addChangeListener (neuer ReadOutSynchronizer ()); } public static void main (String args []) {Test test = new Test (); test.setBounds (100.100.400.350); test.show (); } Klasse ReadOutSynchronizer implementiert ChangeListener {public void stateChanged (ChangeEvent e) {String s = Integer.toString (model.getValue ()); readOut.setText (s + "%"); readOut.revalidate (); }}} Klasse ImageView erweitert JScrollPane {private JPanel panel = new JPanel (); private Dimension originalSize = neue Dimension (); privates Bild originalImage; privates ImageIcon-Symbol; public ImageView (ImageIcon-Symbol, BoundedRangeModel-Modell) {panel.setLayout (neues BorderLayout ()); panel.add (neues JLabel (Symbol)); this.icon = icon; this.originalImage = icon.getImage (); setViewportView (Panel);model.addChangeListener (neuer ModelListener ()); originalSize.width = icon.getIconWidth (); originalSize.height = icon.getIconHeight (); } Klasse ModelListener implementiert 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 (); Doppelmultiplikator = (Doppel-) Wert / (Doppel-) Spanne; Multiplikator = Multiplikator == 0,0? 0,01: Multiplikator; Bild skaliert = originalImage.getScaledInstance ((int) (originalSize.width * Multiplikator), (int) (originalSize.height * Multiplikator), Image.SCALE_FAST); icon.setImage (skaliert); panel.revalidate (); panel.repaint (); }}}}

Wenn Sie den Schieberegler bewegen, ändert der Schieberegler den Wert seines Modells. Diese Änderung löst Ereignisbenachrichtigungen an die beiden im Modell registrierten Änderungslistener aus, die die Anzeige anpassen und das Bild skalieren. Beide Listener verwenden das an übergebene Änderungsereignis

stateChanged()

um den neuen Wert des Modells zu bestimmen.

Swing ist ein starker Benutzer des Observer-Musters. Es implementiert mehr als 50 Ereignis-Listener, um anwendungsspezifisches Verhalten zu implementieren, von der Reaktion auf eine gedrückte Schaltfläche bis zum Veto gegen ein Fensterschließereignis für einen internen Frame. Swing ist jedoch nicht das einzige Framework, das das Observer-Muster sinnvoll einsetzt - es wird im Java 2 SDK häufig verwendet. Beispiel: das Abstract Window Toolkit, das JavaBeans-Framework, das javax.namingPaket und die Eingabe- / Ausgabehandler.

Beispiel 1 zeigt speziell die Verwendung des Observer-Musters mit Swing. Bevor wir weitere Details zum Observer-Muster diskutieren, schauen wir uns an, wie das Muster im Allgemeinen implementiert wird.

So funktioniert das Observer-Muster

Abbildung 2 zeigt, wie Objekte im Observer-Muster zusammenhängen.

Das Thema, das eine Ereignisquelle ist, verwaltet eine Sammlung von Beobachtern und bietet Methoden zum Hinzufügen und Entfernen von Beobachtern zu dieser Sammlung. Das Subjekt implementiert auch eine notify()Methode, die jeden registrierten Beobachter über Ereignisse informiert, die den Beobachter interessieren. Die Probanden benachrichtigen die Beobachter, indem sie die update()Methode des Beobachters aufrufen .

Abbildung 3 zeigt ein Sequenzdiagramm für das Observer-Muster.

In der Regel ruft ein nicht verwandtes Objekt die Methode eines Subjekts auf, mit der der Status des Subjekts geändert wird. In diesem Fall ruft das Subjekt eine eigene notify()Methode auf, die die Sammlung von Beobachtern durchläuft und die update()Methode jedes Beobachters aufruft .

Das Observer-Muster ist eines der grundlegendsten Entwurfsmuster, da es die Kommunikation stark entkoppelter Objekte ermöglicht. In Beispiel 1 weiß das Modell mit begrenztem Bereich nur, dass es eine stateChanged()Methode implementiert . Die Zuhörer interessieren sich nur für den Wert des Modells, nicht für die Implementierung des Modells. Das Modell und seine Zuhörer wissen sehr wenig voneinander, aber dank des Observer-Musters können sie kommunizieren. Durch diesen hohen Grad an Entkopplung zwischen Modellen und Listenern können Sie Software erstellen, die aus steckbaren Objekten besteht, und Ihren Code hochflexibel und wiederverwendbar machen.

Das Java 2 SDK und das Observer-Muster

Das Java 2 SDK bietet eine klassische Implementierung des Observer-Musters mit der ObserverSchnittstelle und der ObservableKlasse aus dem java.utilVerzeichnis. Die ObservableKlasse repräsentiert das Thema; Beobachter implementieren die ObserverSchnittstelle. Interessanterweise wird diese klassische Observer-Musterimplementierung in der Praxis selten verwendet, da die Probanden die ObservableKlasse erweitern müssen. Das Erfordernis der Vererbung ist in diesem Fall ein schlechtes Design, da möglicherweise jeder Objekttyp ein Subjektkandidat ist und Java keine Mehrfachvererbung unterstützt. Oft haben diese Fachkandidaten bereits eine Oberklasse.

Die ereignisbasierte Implementierung des Observer-Musters, die im vorherigen Beispiel verwendet wurde, ist die überwältigende Wahl für die Implementierung des Observer-Musters, da keine Subjekte erforderlich sind, um eine bestimmte Klasse zu erweitern. Stattdessen folgen die Probanden einer Konvention, die die folgenden Registrierungsmethoden für öffentliche Hörer erfordert:

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

Immer wenn sich die gebundene Eigenschaft eines Subjekts (eine Eigenschaft, die von Listenern beobachtet wurde) ändert, durchläuft das Subjekt seine Listener und ruft die von der XXXListenerSchnittstelle definierte Methode auf .

Inzwischen sollten Sie das Observer-Muster gut verstehen. Der Rest dieses Artikels konzentriert sich auf einige der Feinheiten des Observer-Musters.

Anonyme innere Klassen

In Beispiel 1 habe ich innere Klassen verwendet, um die Listener der Anwendung zu implementieren, da die Listener-Klassen eng mit ihrer umschließenden Klasse gekoppelt waren. Sie können Listener jedoch beliebig implementieren. Eine der beliebtesten Optionen für die Behandlung von Benutzeroberflächenereignissen ist die anonyme innere Klasse, eine Klasse ohne Namen, die inline erstellt wurde, wie in Beispiel 2 gezeigt:

Beispiel 2. Implementieren Sie Beobachter mit anonymen inneren Klassen

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

Die Verwendung anonymer innerer Klassen, wie im vorherigen Beispiel gezeigt, war bei Entwicklern sehr beliebt. Ab Java 2 Platform, Standard Edition (J2SE) 1.4 hat die JavaBeans-Spezifikation die Verantwortung für die Implementierung und Instanziierung dieser inneren Klassen für Sie mit der EventHandlerKlasse übernommen. wie in Beispiel 3 gezeigt:

Beispiel 3. Verwenden von java.beans.EventHandler

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