Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung
 <<    <     >    >>  Kapitel 28 - Event-Handling

28.2 Entwurfsmuster für den Nachrichtenverkehr



Wir wollen uns in diesem Abschnitt damit beschäftigen, die oben erwähnten Entwurfsmuster für die Abwicklung des Nachrichtenverkehrs in Java-Programmen vorzustellen. Wie bereits erwähnt, hat jedes dieser Verfahren seine ganz spezifischen Vor- und Nachteile und ist für verschiedene Programmieraufgaben unterschiedlich gut geeignet.

Als Basis für unsere Experimente wollen wir ein einfaches Programm schreiben, das die folgenden Anforderungen erfüllt:

Basis der Programme ist das folgende Listing:

001 /* Listing2801.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2801
007 extends Frame
008 {
009   public static void main(String[] args)
010   {
011     Listing2801 wnd = new Listing2801();
012   }
013 
014   public Listing2801()
015   {
016     super("Nachrichtentransfer");
017     setBackground(Color.lightGray);
018     setSize(300,200);
019     setLocation(200,100);
020     setVisible(true);
021   }
022 
023   public void paint(Graphics g)
024   {
025     g.setFont(new Font("Serif",Font.PLAIN,18));
026     g.drawString("Zum Beenden bitte ESC drücken...",10,50);
027   }
028 }
Listing2801.java
Listing 28.1: Basisprogramm für den Nachrichtentransfer

Das Programm erfüllt die ersten der obengenannten Anforderungen, ist aber mangels Event-Handler noch nicht in der Lage, per [ESC] beendet zu werden. Um die Nachfolgeversionen vorzubereiten, haben wir bereits die Anweisung import java.awt.event.* eingefügt.

 Hinweis 

Die Ausgabe des Programms ist:

Abbildung 28.3: Das Programm für den Nachrichtentransfer

28.2.1 Variante 1: Implementierung eines EventListener-Interfaces

Bei der ersten Variante gibt es nur eine einzige Klasse, Listing2802. Sie ist einerseits eine Ableitung der Klasse Frame, um ein Fenster auf dem Bildschirm darzustellen und zu beschriften. Andererseits implementiert sie das Interface KeyListener, das die Methoden keyPressed, keyReleased und keyTyped definiert. Der eigentliche Code zur Reaktion auf die Taste [ESC] steckt in der Methode keyPressed, die immer dann aufgerufen wird, wenn eine Taste gedrückt wurde. Mit der Methode getKeyCode der Klasse KeyEvent wird auf den Code der gedrückten Taste zugegriffen und dieser mit der symbolischen Konstante VK_ESCAPE verglichen. Stimmen beide überein, wurde [ESC] gedrückt, und das Programm kann beendet werden.

001 /* Listing2802.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2802
007 extends Frame
008 implements KeyListener
009 {
010   public static void main(String[] args)
011   {
012     Listing2802 wnd = new Listing2802();
013   }
014 
015   public Listing2802()
016   {
017     super("Nachrichtentransfer");
018     setBackground(Color.lightGray);
019     setSize(300,200);
020     setLocation(200,100);
021     setVisible(true);
022     addKeyListener(this);
023   }
024 
025   public void paint(Graphics g)
026   {
027     g.setFont(new Font("Serif",Font.PLAIN,18));
028     g.drawString("Zum Beenden bitte ESC drücken...",10,50);
029   }
030 
031   public void keyPressed(KeyEvent event)
032   {
033     if (event.getKeyCode() == KeyEvent.VK_ESCAPE) {
034       setVisible(false);
035       dispose();
036       System.exit(0);
037     }
038   }
039 
040   public void keyReleased(KeyEvent event)
041   {
042   }
043 
044   public void keyTyped(KeyEvent event)
045   {
046   }
047 }
Listing2802.java
Listing 28.2: Implementieren eines Listener-Interfaces

Die Verbindung zwischen der Ereignisquelle (in diesem Fall der Fensterklasse Listing2802) und dem Ereignisempfänger (ebenfalls die Klasse Listing2802) erfolgt über den Aufruf der Methode addKeyListener der Klasse Frame. Alle Tastaturereignisse werden dadurch an die Fensterklasse selbst weitergeleitet und führen zum Aufruf der Methoden keyPressed, keyReleased oder keyTyped des Interfaces KeyListener. Diese Implementierung ist sehr naheliegend, denn sie ist einfach zu implementieren und erfordert keine weiteren Klassen. Nachteilig ist dabei allerdings:

Es bleibt festzuhalten, daß diese Technik bestenfalls für kleine Programme geeignet ist, die nur begrenzt erweitert werden müssen. Durch die Vielzahl leerer Methodenrümpfe können aber auch kleine Programme schnell unübersichtlich werden.

28.2.2 Variante 2: Lokale und anonyme Klassen

Die zweite Alternative bietet eine bessere Lösung. Sie basiert auf der Verwendung lokaler bzw. anonymer Klassen und kommt ohne die Nachteile der vorigen Version aus. Sie ist das in der Dokumentation des JDK empfohlene Entwurfsmuster für das Event-Handling in kleinen Programmen oder bei Komponenten mit einfacher Nachrichtenstruktur. Vor ihrem Einsatz sollte man allerdings das Prinzip lokaler und anonymer Klassen kennenlernen, das mit dem JDK 1.1 in Java eingeführt und in Abschnitt 10.1 vorgestellt wurde. Wer diesen Abschnitt noch nicht gelesen hat, sollte das jetzt nachholen.

Lokale Klassen

Die Anwendung lokaler Klassen für die Ereignisbehandlung besteht darin, mit ihrer Hilfe die benötigten EventListener zu implementieren. Dazu wird in dem GUI-Objekt, das einen Event-Handler benötigt, eine lokale Klasse definiert und aus einer passenden Adapterklasse abgeleitet. Nun braucht nicht mehr das gesamte Interface implementiert zu werden (denn die Methodenrümpfe werden ja aus der Adapterklasse geerbt), sondern lediglich die tatsächlich benötigten Methoden. Da die lokale Klasse zudem auf die Membervariablen und Methoden der Klasse zugreifen kann, in der sie definiert wurde, lassen sich auf diese Weise sehr schnell die benötigten Ereignisempfänger zusammenbauen.

Das folgende Beispiel definiert eine lokale Klasse MyKeyListener, die aus KeyAdapter abgeleitet wurde und auf diese Weise das KeyListener-Interface implementiert. Sie überlagert lediglich die Methode keyPressed, um auf das Drücken einer Taste zu reagieren. Als lokale Klasse hat sie außerdem Zugriff auf die Methoden der umgebenden Klasse und kann somit durch Aufruf von setVisible und dispose das Fenster, in dem sie als Ereignisempfänger registriert wurde, schließen. Die Registrierung der lokalen Klasse erfolgt durch Aufruf von addKeyListener, bei dem gleichzeitig eine Instanz der lokalen Klasse erzeugt wird. Als lokale Klasse ist MyKeyListener überall innerhalb von Listing2803 sichtbar und kann an beliebiger Stelle instanziert werden.

001 /* Listing2803.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2803
007 extends Frame
008 {
009   public static void main(String[] args)
010   {
011     Listing2803 wnd = new Listing2803();
012   }
013 
014   public Listing2803()
015   {
016     super("Nachrichtentransfer");
017     setBackground(Color.lightGray);
018     setSize(300,200);
019     setLocation(200,100);
020     setVisible(true);
021     addKeyListener(new MyKeyListener());
022   }
023 
024   public void paint(Graphics g)
025   {
026     g.setFont(new Font("Serif",Font.PLAIN,18));
027     g.drawString("Zum Beenden bitte ESC drücken...",10,50);
028   }
029 
030   class MyKeyListener
031   extends KeyAdapter
032   {
033     public void keyPressed(KeyEvent event)
034     {
035       if (event.getKeyCode() == KeyEvent.VK_ESCAPE) {
036         setVisible(false);
037         dispose();
038         System.exit(0);
039       }
040     }
041   }
042 }
Listing2803.java
Listing 28.3: Verwendung lokaler Klassen

Der Vorteil dieser Vorgehensweise ist offensichtlich: es werden keine unnützen Methodenrümpfe erzeugt, aber trotzdem verbleibt der Ereignisempfängercode wie im vorigen Beispiel innerhalb der Ereignisquelle. Dieses Verfahren ist also immer dann gut geeignet, wenn es von der Architektur oder der Komplexität der Ereignisbehandlung her sinnvoll ist, Quelle und Empfänger zusammenzufassen.

Anonyme Klassen

Das folgende Beispiel ist eine leichte Variation des vorigen. Es zeigt die Verwendung einer anonymen Klasse, die aus KeyAdapter abgeleitet wurde, als Ereignisempfänger. Zum Instanzierungszeitpunkt erfolgt die Definition der überlagernden Methode keyPressed, in der der Code zur Reaktion auf das Drücken der Taste [ESC] untergebracht wird.

001 /* Listing2804.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2804
007 extends Frame
008 {
009   public static void main(String[] args)
010   {
011     Listing2804 wnd = new Listing2804();
012   }
013 
014   public Listing2804()
015   {
016      super("Nachrichtentransfer");
017      setBackground(Color.lightGray);
018      setSize(300,200);
019      setLocation(200,100);
020      setVisible(true);
021      addKeyListener(
022        new KeyAdapter() {
023          public void keyPressed(KeyEvent event)
024          {
025            if (event.getKeyCode() == KeyEvent.VK_ESCAPE) {
026              setVisible(false);
027              dispose();
028              System.exit(0);
029            }
030          }
031        }
032      );
033    }
034 
035   public void paint(Graphics g)
036   {
037     g.setFont(new Font("Serif",Font.PLAIN,18));
038     g.drawString("Zum Beenden bitte ESC drücken...",10,50);
039   }
040 }
Listing2804.java
Listing 28.4: Verwendung einer anonymen Klasse als Ereignishandler

Vorteilhaft bei dieser Vorgehensweise ist der verminderte Aufwand, denn es muß keine separate Klassendefinition angelegt werden. Statt dessen werden die wenigen Codezeilen, die zur Anpassung der Adapterklasse erforderlich sind, dort eingefügt, wo die Klasse instanziert wird, nämlich beim Registrieren des Nachrichtenempfängers. Anonyme Klassen haben einen ähnlichen Einsatzbereich wie lokale, empfehlen sich aber vor allem, wenn sehr wenig Code für den Ereignisempfänger benötigt wird. Bei aufwendigeren Ereignisempfängern ist die explizite Definition einer benannten Klasse dagegen vorzuziehen.

28.2.3 Variante 3: Trennung von GUI- und Anwendungscode

Wir hatten am Anfang darauf hingewiesen, daß in größeren Programmen eine Trennung zwischen Programmcode, der für die Oberfläche zuständig ist, und solchem, der für die Anwendungslogik zuständig ist, wünschenswert wäre. Dadurch wird eine bessere Modularisierung des Programms erreicht, und der Austausch oder die Erweiterung von Teilen des Programms wird erleichtert.

Das Delegation Event Model wurde auch mit dem Designziel entworfen, eine solche Trennung zu ermöglichen bzw. zu erleichtern. Der Grundgedanke dabei war es, auch Nicht-Komponenten die Reaktion auf GUI-Events zu ermöglichen. Dies wurde dadurch erreicht, daß jede Art von Objekt als Ereignisempfänger registriert werden kann, solange es die erforderlichen Listener-Interfaces implementiert. Damit ist es möglich, die Anwendungslogik vollkommen von der grafischen Oberfläche abzulösen und in Klassen zu verlagern, die eigens für diesen Zweck entworfen wurden.

 Hinweis 

Das nachfolgende Beispiel zeigt diese Vorgehensweise, indem es unser Beispielprogramm in die drei Klassen Listing2805, MainFrameCommand und MainFrameGUI aufteilt. Listing2805 enthält nur noch die main-Methode und dient lediglich dazu, die anderen beiden Klassen zu instanzieren. MainFrameGUI realisiert die GUI-Funktionalität und stellt das Fenster auf dem Bildschirm dar. MainFrameCommand spielt die Rolle des Kommandointerpreters, der immer dann aufgerufen wird, wenn im Fenster ein Tastaturereignis aufgetreten ist.

Die Verbindung zwischen beiden Klassen erfolgt durch Aufruf der Methode addKeyListener in MainFrameGUI, an die das an den Konstruktor übergebene MainFrameCommand-Objekt weitergereicht wird. Dazu ist es erforderlich, daß das Hauptprogramm den Ereignisempfänger cmd zuerst instanziert, um ihn bei der Instanzierung des GUI-Objekts gui übergeben zu können.

Umgekehrt benötigt natürlich auch das Kommando-Objekt Kenntnis über das GUI-Objekt, denn es soll ja das zugeordnete Fenster schließen und das Programm beenden. Der scheinbare Instanzierungskonflikt durch diese zirkuläre Beziehung ist aber in Wirklichkeit gar keiner, denn bei jedem Aufruf einer der Methoden von MainFrameCommand wird an das KeyEvent-Objekt der Auslöser der Nachricht übergeben, und das ist in diesem Fall stets das MainFrameGUI-Objekt gui. So kann innerhalb des Kommando-Objekts auf alle öffentlichen Methoden des GUI-Objekts zugegriffen werden.

 Tip 

001 /* Listing2805.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2805
007 {
008   public static void main(String[] args)
009   {
010     MainFrameCommand cmd = new MainFrameCommand();
011     MainFrameGUI     gui = new MainFrameGUI(cmd);
012   }
013 }
014 
015 class MainFrameGUI
016 extends Frame
017 {
018   public MainFrameGUI(KeyListener cmd)
019   {
020     super("Nachrichtentransfer");
021     setBackground(Color.lightGray);
022     setSize(300,200);
023     setLocation(200,100);
024     setVisible(true);
025     addKeyListener(cmd);
026   }
027 
028   public void paint(Graphics g)
029   {
030     g.setFont(new Font("Serif",Font.PLAIN,18));
031     g.drawString("Zum Beenden bitte ESC drücken...",10,50);
032   }
033 }
034 
035 class MainFrameCommand
036 implements KeyListener
037 {
038   public void keyPressed(KeyEvent event)
039   {
040     Frame source = (Frame)event.getSource();
041     if (event.getKeyCode() == KeyEvent.VK_ESCAPE) {
042       source.setVisible(false);
043       source.dispose();
044       System.exit(0);
045     }
046   }
047 
048   public void keyReleased(KeyEvent event)
049   {
050   }
051 
052   public void keyTyped(KeyEvent event)
053   {
054   }
055 }
Listing2805.java
Listing 28.5: Trennung von GUI- und Anwendungslogik

Diese Designvariante ist vorwiegend für größere Programme geeignet, bei denen eine Trennung von Programmlogik und Oberfläche sinnvoll ist. Für sehr kleine Programme oder solche, die wenig Ereigniscode haben, sollte eher eine der vorherigen Varianten angewendet werden, wenn diese zu aufwendig ist. Sie entspricht in groben Zügen dem Mediator-Pattern, das in "Design-Patterns" von Gamma et al. beschrieben wird.

Natürlich erhebt das vorliegende Beispielprogramm nicht den Anspruch, unverändert in ein sehr großes Programm übernommen zu werden. Es soll lediglich die Möglichkeit der Trennung von Programmlogik und Oberfläche in einem großen Programm mit Hilfe der durch das Event-Handling des JDK 1.1 vorgegebenen Möglichkeiten aufzeigen. Eine sinnvolle Erweiterung dieses Konzepts könnte darin bestehen, weitere Modularisierungen vorzunehmen (z.B. analog dem MVC-Konzept von Smalltalk, bei dem GUI-Anwendungen in Model-, View- und Controller-Layer aufgesplittet werden, oder auch durch Abtrennen spezialisierter Kommandoklassen). Empfehlenswert ist in diesem Zusammenhang die Lektüre der JDK-Dokumentation, die ein ähnliches Beispiel in leicht veränderter Form enthält.

28.2.4 Variante 4: Überlagern der Event-Handler in den Komponenten

Als letzte Möglichkeit, auf Nachrichten zu reagieren, soll das Überlagern der Event-Handler in den Ereignisquellen selbst aufgezeigt werden. Jede Ereignisquelle besitzt eine Reihe von Methoden, die für das Aufbereiten und Verteilen der Nachrichten zuständig sind. Soll eine Nachricht weitergereicht werden, so wird dazu zunächst innerhalb der Nachrichtenquelle die Methode processEvent aufgerufen. Diese verteilt die Nachricht anhand ihres Typs an spezialisierte Methoden, deren Name sich nach dem Typ der zugehörigen Ereignisklasse richtet. So ist beispielsweise die Methode processActionEvent für das Handling von Action-Events und processMouseEvent für das Handling von Mouse-Events zuständig:

protected void processEvent(AWTEvent e)

protected void processComponentEvent(ComponentEvent e)

protected void processFocusEvent(FocusEvent e)

...
java.awt.Component

Beide Methodenarten können in einer abgeleiteten Klasse überlagert werden, um die zugehörigen Ereignisempfänger zu implementieren. Wichtig ist dabei, daß in der abgeleiteten Klasse die gleichnamige Methode der Basisklasse aufgerufen wird, um das Standardverhalten sicherzustellen. Wichtig ist weiterhin, daß sowohl processEvent als auch processActionEvent usw. nur aufgerufen werden, wenn der entsprechende Ereignistyp für diese Ereignisquelle aktiviert wurde. Dies passiert in folgenden Fällen:

  • Wenn ein passender Ereignisempfänger über die zugehörige addEventListener-Methode registriert wurde.
  • Wenn der Ereignistyp explizit durch Aufruf der Methode enableEvents aktiviert wurde.
 Warnung 

Die Methode enableEvents erwartet als Argument eine Maske, die durch eine bitweise Oder-Verknüpfung der passenden Maskenkonstanten aus der Klasse AWTEvent zusammengesetzt werden kann:

protected final void enableEvents(long eventsToEnable)
java.awt.Component

Die verfügbaren Masken sind analog zu den Ereignistypen benannt und heißen ACTION_EVENT_MASK, ADJUSTMENT_EVENT_MASK, COMPONENT_EVENT_MASK usw.

Das folgende Beispiel überlagert die Methode processKeyEvent in der Klasse Frame (die sie aus Component geerbt hat). Durch Aufruf von enableEvents wird die Weiterleitung der Tastaturereignisse aktiviert, und das Programm zeigt dasselbe Verhalten wie die vorigen Programme.

001 /* Listing2806.java */
002 
003 import java.awt.*;
004 import java.awt.event.*;
005 
006 public class Listing2806
007 extends Frame
008 {
009   public static void main(String[] args)
010   {
011     Listing2806 wnd = new Listing2806();
012   }
013 
014   public Listing2806()
015   {
016     super("Nachrichtentransfer");
017     setBackground(Color.lightGray);
018     setSize(300,200);
019     setLocation(200,100);
020     setVisible(true);
021     enableEvents(AWTEvent.KEY_EVENT_MASK);
022   }
023 
024   public void paint(Graphics g)
025   {
026     g.setFont(new Font("Serif",Font.PLAIN,18));
027     g.drawString("Zum Beenden bitte ESC drücken...",10,50);
028   }
029 
030   public void processKeyEvent(KeyEvent event)
031   {
032     if (event.getID() == KeyEvent.KEY_PRESSED) {
033       if (event.getKeyCode() == KeyEvent.VK_ESCAPE) {
034         setVisible(false);
035         dispose();
036         System.exit(0);
037       }
038     }
039     super.processKeyEvent(event);
040   }
041 }
Listing2806.java
Listing 28.6: Überlagern der Komponenten-Event-Handler

Diese Art der Ereignisbehandlung ist nur sinnvoll, wenn Fensterklassen oder Dialogelemente überlagert werden und ihr Aussehen oder Verhalten signifikant verändert wird. Alternativ könnte natürlich auch in diesem Fall ein EventListener implementiert und die entsprechenden Methoden im Konstruktor der abgeleiteten Klasse registriert werden.

Das hier vorgestellte Verfahren umgeht das Delegation Event Model vollständig und hat damit die gleichen inhärenten Nachteile wie das Event-Handling des alten JDK. Die Dokumentation zum JDK empfiehlt daher ausdrücklich, für alle »normalen« Anwendungsfälle das Delegation Event Model zu verwenden und die Anwendungen nach einem der in den ersten drei Beispielen genannten Entwurfsmuster zu implementieren.

 Hinweis 

28.2.5 Ausblick

Die hier vorgestellten Entwurfsmuster geben einen Überblick über die wichtigsten Designtechniken für das Event-Handling in Java-Programmen. Während die ersten beiden Beispiele für kleine bis mittelgroße Programme gut geeignet sind, kommen die Vorteile der in Variante 3 vorgestellten Trennung zwischen GUI-Code und Anwendungslogik vor allem bei größeren Programmen zum Tragen. Die vierte Variante ist vornehmlich für Spezialfälle geeignet und sollte entsprechend umsichtig eingesetzt werden.

Wir werden in den nachfolgenden Kapiteln vorwiegend die ersten beiden Varianten einsetzen. Wenn es darum geht, Ereignishandler für die Beispielprogramme zu implementieren, werden wir also entweder die erforderlichen Listener-Interfaces in der Fensterklasse selbst implementieren oder sie in lokalen oder anonymen Klassen unterbringen. Da das Event-Handling des JDK 1.1 eine Vielzahl von Designvarianten erlaubt, werden wir uns nicht immer sklavisch an die vorgestellten Entwurfsmuster halten, sondern teilweise leicht davon abweichen oder Mischformen verwenden. Dies ist beabsichtigt und soll den möglichen Formenreichtum demonstrieren. Wo nötig, werden wir auf spezielle Implementierungsdetails gesondert eingehen.

 Hinweis 

Kapitel 29 widmet sich den wichtigsten Low-Level-Events und demonstriert den genauen Einsatz ihrer Listener- und Event-Methoden anhand vieler Beispiele. In späteren Kapiteln werden die meisten der High-Level-Events erläutert. Sie werden in der Regel dort eingeführt, wo ihr Einsatz durch das korrespondierende Dialogelement motiviert wird. So erläutert Kapitel 30 in Zusammenhang mit der Vorstellung von Menüs die Action-Ereignisse, und in Kapitel 32 werden Ereignisse erläutert, die von den dort vorgestellten Dialogelementen ausgelöst werden.


 Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0
 <<    <     >    >>  © 2000 Guido Krüger, http://www.gkrueger.com