Java Getriebe

Java und NetBeans

Kleine Datenverwaltung mit ObservableProperties

Daten. Fluch und Segen der Softwareentwicklung. Ohne das eine würde es das andere nicht geben. Die Kunst ist, mit diesen Daten richtig umzugehen. Manch einer arbeitet daran ein Leben lang. Manche glauben sie gehören nicht zu dieser Gruppe.

Ein Problem, vor dem man immer wieder steht und ich merke, dass gerade Programmieranfänger in der Oberflächenprogrammierung sehr häufig vor der Frage stehen: „Wie bekomme ich es denn hin, dass das eine UI Element an die Daten des anderen UI Elements kommt?“ Vielleicht ohne es zu merken hat der Neuling die zentrale Frage 1 bei der Oberflächenprogrammierung gestellt. Denn genau das ist das Hauptproblem bei dieser Software: Daten von A nach B kopieren um dort weiter verarbeitet zu werden. Die Schwierigkeit besteht meist darin, dass A von B nicht unbedingt etwas weiß.

An dieser Stelle wird meistens das so genannte Model-View-Controller (MVC) Konzept angeführt. Also die Trennung in eine Datenschicht (Model), der Anzeige (View), und den Benutzereingaben (Controller). Häufig werden View und Controller aber nicht so deutlich getrennt, so dass man in der Praxis häufig eine Zweiteilung hat. Entscheidend ist vor allem, dass die Daten vollständig getrennt von dem Rest der Anwendung stehen.

In diesem Artikel möchte ich eine kleine Hilfsklasse zeigen, die gerade in kleineren Anwendungen unheimlich viel Arbeit ersparen kann und auf eine recht simple Art und Weise die Datentrennung bewerkstelligt: ObservableProperties

public class ObservableProperties<V> extends HashMap<String, V> {
  private final PropertyChangeSupport pcs;

  public ObservableProperties() {
    super();
    pcs = new PropertyChangeSupport(this);
  }

  [...]
}

Grundlage dieser Klasse ist eine Standard [javadocx]java.util.HashMap[/javadocx] mit einem [javadocx]java.lang.String[/javadocx] als Schlüssel. Welche Daten letztendlich gespeichert werden ist für die Funktionsweise nicht relevant. Das Generics <V> lässt uns auch noch genügend Freiheiten die Klasse für die verschiedensten Anwendungsfälle einzusetzen. Der Name Properties mag vielleicht etwas irreführend sein, da man nicht selten damit eine <String, String> Beziehung verknüpft. Dennoch beschränken sich Properties nicht unbedingt auf String Werte, sondern durch den Namen wird viel mehr definiert, dass der Schlüssel ein String ist.

Die größte Zauberei der Klasse wird eigentlich von der Klassenvariable pcs erledigt. Die Klasse [javadocx]java.beans.PropertyChangeSupport[/javadocx] stellt alle Funktionen bereit um ein „Observer Pattern“ zu benutzen. Bei diesem „Entwurfsmuster“ (Pattern) geht es darum, dass das überwachte Element (die Daten) nicht direkt weiß, wer es alles überwachen will (die UI Elemente). Viel mehr agieren die Daten als eine Art „Dienstleister“, bei dem sich andere Klassen anmelden können und der „Dienstleister“ einfach allen angemeldeten Instanzen eine Mitteilung zukommen lässt, wenn er selbst (also die Daten) sich geändert hat. In Java passiert diese „Anmeldung“ beim Dienstleister als [javadocx]java.beans.PropertyChangeListener[/javadocx]. Wer einmal in die JavaDocs der Klasse unter „All Known Implementing Classes“ schaut wird vielleicht merken, dass dieses Interface ziemlich häufig innerhalb von Swing eingesetzt wird. Nicht zu Unrecht.

Damit unsere eigene Klasse davon profitieren kann müssen wir nichts anderen tun, als entsprechende Delegatemethoden zu erzeugen, die an den PropertyChangeSupport weiter reichen.

public synchronized void addPropertyChangeListener(
    PropertyChangeListener listener) {
  pcs.addPropertyChangeListener(listener);
}

public synchronized void removePropertyChangeListener(
    PropertyChangeListener listener) {
  pcs.removePropertyChangeListener(listener);
}

Und schon ist die Datenklasse der beschriebene Dienstleister bei dem sich „alle Welt“ an- und abmelden kann. Die Datenklasse selbst muss keinerlei direkte Kenntnis von den Klassen haben, die Informationen von ihr auslesen wollen.

Als nächster Schritt kommt der Teil mit der „Mitteilung“. Auch hierfür stellt PropertyChangeSupport passende Methoden bereit. Wir müssen nur jedes Mal, wenn sich unsere Daten ändern diese Methoden aufrufen. Die beste Stelle dazu ist die Methode put aus dem [javadocx]java.util.Map[/javadocx], die wir überschreiben:

@Override
public synchronized V put(String key, V value) {
  V old = super.put(key, value);
  pcs.firePropertyChange(key, old, value);
  return old;
}

firePropertyChange bekommt immer den Namen des Properties, den bisherigen Wert, der unter diesem Schlüssel gespeichert war und den jetzt aktuellen Wert übergeben. Diese Daten werden dann automatisch an alle registrierten PropertyChangeListener weiter gereicht, die diese Information aus dem entsprechenden Event-Object der propertyChange Methode auslesen können.

Das gleiche müssen wir auch erledigen, wenn ein Element aus den Daten gelöscht wird. Hier entspricht der jetzt aktuelle Wert null:

@Override
public V remove(Object key) {
  V old = super.remove(key);
  if(key instanceof String)
    pcs.firePropertyChange((String)key, old, null);
  return old;
}

Leider ist bei dieser Methode den Javaentwicklern ein kleiner Designfehler unterlaufen. Statt dass der Parameter auch per Generic definiert wurde wird hier ein Object erwartet und wird sich gezwungen ein unschönen Cast durchzuführen. Um zu vermeiden, dass jemand unsere Klasse falsch verwendet und unschöne Exceptions auftreten erfolgt vorher noch eine passende instanceof Abfrage.

Damit ist die Datenklasse auch schon komplett. Die Oberflächenelement können sich nun bei der Datenklasse anmelden. Sollte ein Wert von einem Element A geändert werden bekommt Element B automatisch eine Nachricht über diese Änderung und kann entsprechend reagieren. Das Tolle ist, dass A und B nichts voneinander wissen müssen 2.

Im Anschluss noch die gesamte Klasse. Es sind ein paar mehr Delegatermethoden von PropertyChangeSupport implementiert, die die Benutzung etwas weiter erleichtern und noch die clear Methode überschrieben. Diese ruft intern nicht remove auf, sondern löscht den internen Speicher der HashMap direkt. Trotzdem müssen wir für jeden key ein PropertyChangeEvent auslösen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package de.nigjo.observprops;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Eine Datenklasse, die per PropertyChangeListener ueberwacht
 * werden kann.
 *
 * @author Jens Hofschroeer
 */

public class ObservableProperties<V> extends HashMap<String, V>
{
  private static final long serialVersionUID = -7552606273696340366L;
  private final PropertyChangeSupport pcs;

  public ObservableProperties()
  {
    super();
    pcs = new PropertyChangeSupport(this);
  }

  //<editor-fold defaultstate="collapsed" desc="delegate PropertyChangeSupport">
  public synchronized void addPropertyChangeListener(
      PropertyChangeListener listener)
  {
    pcs.addPropertyChangeListener(listener);
  }

  public synchronized void removePropertyChangeListener(
      PropertyChangeListener listener)
  {
    pcs.removePropertyChangeListener(listener);
  }

  public synchronized PropertyChangeListener[] getPropertyChangeListeners()
  {
    return pcs.getPropertyChangeListeners();
  }

  public synchronized void addPropertyChangeListener(String propertyName,
      PropertyChangeListener listener)
  {
    pcs.addPropertyChangeListener(propertyName, listener);
  }

  public synchronized void removePropertyChangeListener(String propertyName,
      PropertyChangeListener listener)
  {
    pcs.removePropertyChangeListener(propertyName, listener);
  }

  public synchronized PropertyChangeListener[] getPropertyChangeListeners(
      String propertyName)
  {
    return pcs.getPropertyChangeListeners(propertyName);
  }

  public synchronized boolean hasListeners(String propertyName)
  {
    return pcs.hasListeners(propertyName);
  }
  //</editor-fold>

  @Override
  public synchronized V put(String key, V value)
  {
    V old = super.put(key, value);
    pcs.firePropertyChange(key, old, value);
    return old;
  }

  @Override
  public V remove(Object key)
  {
    V old = super.remove(key);
    pcs.firePropertyChange((String)key, old, null);
    return old;
  }

  @Override
  public void clear()
  {
    //kopie erstellen fuer PCS
    Map<String, V> oldData = new HashMap<>(this);
    super.clear();
    for(Entry<String, V> entry : oldData.entrySet())
    {
      pcs.firePropertyChange(entry.getKey(), entry.getValue(), null);
    }
  }

}

Notes:

  1. in meinen Augen
  2. *jippie*

3 Responses to Kleine Datenverwaltung mit ObservableProperties

  1. Michael says:

    Ich bin da ja immer etwas skeptisch, die Collections zu erweitern und würde das eher zusammensetzen… (also mit einer Map als Delegate).

  2. Dann müsstest du aber auch konsequent sein und die delegierte Map mit übergeben, damit man je nach Wunsch zum Beispiel auch eine LinkedHashMap oder eine TreeMap nutzen zu könnte. Dann verlierst du aber die Überwachungsmöglichkeit. Was ist, wenn jemand die delegierte Map direkt bearbeitet? So oder so hast du Vor- und Nachteile.

  3. Thomas Wendler says:

    Hallo Jens,
    Wenn Du eine striktere Trennung von Model & View haben willst, solltest Du Dir mal das MVP Pattern von Fowler angucken.
    Vg Thomas