Gegeben sei folgendes Szenario: Der FAZ-Verlag versendet die jeweils aktuelle Zeitung an seine Abonnenten Familie Fischer und Meier. Bei jeder neuen Zeitung soll eine Auslieferung erfolgen.
Eine "Quick und Dirty"-Lösung würde die Klasse FAZVerlag modellieren, die eine Referenz auf einmal Familie Fischer und einmal Familie Meier hält. Beide Familienklassen besitzen eine Schnittstelle mit der der FAZVerlag seine Zeitung an sie übermitteln kann (erhalteZeitung(Zeitung)).
Bemerkung: Natürlich ist es weniger sinnvoll, für Familie Fischer und Meier zwei verschiedene Klassen zu erstellen. Klüger wäre es, einfach eine Klasse "Familie" mit einem Namen-Attribut ("Fischer", "Meier") zu erstellen. Aus didaktischen Gründen sei dieser Gedanke hier allerdings weiter verfolgt.Es geht darum, dass die Abonnenten von verschiedenen Klassen sind.
Code von FAZVerlag:
public class FAZVerlag {
private Zeitung aktuelleZeitung;
private FamilieFischer famFischer;
private FamilieMeier famMeier;
//Nach dem einen neue FAZAusgabe gesetzt wurde
public void verteileZeitung() {
famFischer.erhalteZeitung(aktuelleZeitung);
famMeier.erhalteZeitung(aktuelleZeitung);
}
}
Unschwer zu erkennen, dass dies eine suboptimale Lösung ist. Die Nachteile liegen auf der Hand:
Es seien nun zwei fundamentale OO-Entwurfsprinzipien in Betracht gezogen:
Programmiere auf Schnittstellen, nie auf Implementierungen.
Identifiziere jene Aspekte, die sich ändern und trenne sie von jenen, die konstant bleiben.
Was bleibt konstant? Der Aufruf von erhalteZeitung(Zeitung) auf jedem Abonnenten. Aus den Abonnenten lässt sich somit eine gemeinsame Schnittstelle abstrahieren: Ein Interface Abonnent mit der Methode erhalteZeitung(Zeitung).

Und was sollte variabel sein? Die Abonnenten, die wir dynamisch hinzufügen und entfernen wollen. Dank der Kapselung unserer konkreten Abonnenten hinter der Abonnentschnittstelle ist es nun möglich, dass der FAZVerlag nur noch das Interface kennt. Er interessiert sich nicht dafür, wer seine Abonnenten wirklich sind. Er weißt nur, dass sie Abonnenten sind und damit die Methode erhalteZeitung(FAZAusgabe) implementieren. Durch diese Entkopplung der FAZVerlag von den konkreten Abonnenten kann FAZVerlag alle Abonnenten in einer simplen Liste vorhalten. Melden sich Abonnenten an (aboHinzufügen(IAbonnent)) so werden sie der Liste hinzugefügt, melden sich welche ab (aboEntfernen(IAbonnent)), so werden sie von der Liste entfernt. Soll eine neue Zeitung verteilt werden (verteileZeitung()), so muss lediglich über die Liste aller Abonnenten iteriert und auf jedem die erhalteZeitung(Zeitung)-Methode aufgerufen werden. Die Methode verteileZeitung() wird mit der zu verteilenden Zeitung parametrisiert.

Quellcode FAZVerlag:
public class FAZVerlag{
private List<IAbonnent> abonnentenList = new ArrayList<IAbonnent>();
private Zeitung aktuelleZeitung;
public void aboHinzufuegen(IAbonnent pIAbonnent) {
abonnentenList.add(pIAbonnent);
}
public void aboEntfernen(IAbonnent pIAbonnent) {
abonnentenList.remove(pIAbonnent);
}
private void verteileZeitung(Zeitung pZeitung) {
for (IAbonnent abonnent : abonnentenList) {
abonnent.erhalteZeitung(pZeitung);
}
}
public void setAktuelleZeitung(Zeitung pAktuelleZeitung) {
aktuelleZeitung = pAktuelleZeitung;
//Nach dem einen neue Zeitung gesetzt wurde, werden alle Abonnenten benachrichtigt.
verteileZeitung(aktuelleZeitung);
}
public Zeitung getAktuelleZeitung() {
return aktuelleZeitung;
}
}
Allerdings ist unsere FAZVerlag-Klasse noch nicht kohäsiv, da die Administrations- und Aktualisierungsmethoden mit den FAZspezifischen (getAktuelleZeitung(), setAktuelleZeitung()) vermischt sind. Diese Administrationsmethoden können für jeden möglichen Verlag genutzt werden, sind damit abstrahierbar und in eine abstrakte Superklasse auslagerbar. Somit können wir später einen neuen Verlag neben der FAZ modellieren und vorhanden Code wiederverwenden.

Unser neues Klassendesign ist damit fertig gestellt. In der Zusammenschau ergibt sich folgendes Diagramm:
Quellcode AVerlag, FAZVerlag:
public abstract class AVerlag {
private List<IAbonnent> abonnentenList = new ArrayList<IAbonnent>();
public void aboHinzufuegen(IAbonnent pIAbonnent) {
abonnentenList.add(pIAbonnent);
}
public void aboEntfernen(IAbonnent pIAbonnent) {
abonnentenList.remove(pIAbonnent);
}
protected void verteileZeitung(Zeitung pZeitung) {
for (IAbonnent abonnent : abonnentenList) {
abonnent.erhalteZeitung(pZeitung);
}
}
}
public class FAZVerlag extends AVerlag {
private Zeitung aktuelleZeitung;
public void setAktuelleZeitung(Zeitung pAktuelleZeitung) {
aktuelleZeitung = pAktuelleZeitung;
//Nach dem einen neue Zeitung gesetzt wurde, werden alle Abonnenten benachrichtigt.
verteileZeitung(aktuelleZeitung);
}
public Zeitung getAktuelleZeitung() {
return aktuelleZeitung;
}
}
Quellcode Zeitung:
public class Zeitung {
//Ein examplarisches Field.
private final String titel;
public Zeitung(String pTitel) {
titel = pTitel;
}
public String getTitel() {
return titel;
}
}
Quellcode IAbonnent mit den Realisierungen FamilieFischer, FamilieMeier und FirmaXY:
interface IAbonnent {
public void erhalteZeitung(Zeitung pZeitung);
}
class FamilieFischer implements IAbonnent {
public void erhalteZeitung(Zeitung pZeitung) {
System.out.println("Familie Fischer erhielt die aktuelle Zeitung: " + pZeitung.getTitel());
}
}
class FamilieMeier implements IAbonnent {
public void erhalteZeitung(Zeitung pZeitung) {
System.out.println("Familie Meier erhielt die aktuelle Zeitung: " + pZeitung.getTitel());
}
}
class FirmaXY implements IAbonnent {
public void erhalteZeitung(Zeitung pZeitung) {
System.out.println("Firma XY erhielt die aktuelle Zeitung: " + pZeitung.getTitel());
}
}
Beispielclient:
public class Beispielclient {
public static void main(String[] args) {
FAZVerlag verlag = new FAZVerlag();
verlag.aboHinzufuegen(new FamilieFischer());
verlag.aboHinzufuegen(new FamilieMeier());
FirmaXY firma = new FirmaXY();
verlag.aboHinzufuegen(firma);
verlag.setAktuelleZeitung(new Zeitung("Skandal!"));
//Familie Fischer erhielt die aktuelle Zeitung: Skandal!
//Familie Meier erhielt die aktuelle Zeitung: Skandal!
//Firma XY erhielt die aktuelle Zeitung: Skandal!
verlag.aboEntfernen(firma);
verlag.setAktuelleZeitung(new Zeitung("Doch alles halb so wild!"));
//Familie Fischer erhielt die aktuelle Zeitung: Doch alles halb so wild!
//Familie Meier erhielt die aktuelle Zeitung: Doch alles halb so wild!
}
}
Nun erfreuen wir uns der gewonnen Vorteile:

Es zeigt sich, dass durch den Einsatz des Observer Patterns, ein hohes Maß an Flexibilität und Dynamik gewonnen wird, während zeitgleich die Wartung erleichtert wird und Erweiterungen schnell und unkompliziert möglich sind.
Nach dieser Einführung wird im folgenden Abschnitt das Observer Design Pattern formalisiert, näher analysiert und diskutiert.
| Klasse | Observer Teilnehmer |
|---|---|
| FAZVerlag | Subject (oder Observable) |
| Abonnent | Observer (oder Dependents) |
| verteileZeitung(Zeitung) | Methode mit der die Benachrichtigung aller Observer ausgelöst wird. |
| erhalteZeitung() | Aktualisierungsmethode (oder Updatemethode, Benachrichtigungsmethode) der Observer |
Im Zeitungsbeispiel wurde das sogenannte Push-Modell realisiert. Dazu später mehr.
Observer:
"Definiere eine 1-zu-n-Abhängigkeit zwischen Objekten, so dass die Änderung des Zustands eines Objekts dazu führt, das alle abhängigen Objekte benachrichtigt und automatisch aktualisiert werden."
([GoF], Seite 287)
Das Observer Pattern ermöglicht, dass sich Objekte (Observer, beobachtendes Objekt) bei einem anderem Objekt (Subject, beobachtetes Objekt) registrieren und fortan vom diesem informiert werden, sobald es sich ändert.
Für die Observer wird eine einheitliche Schnittstelle (Interface) mit mindestens einer Aktualisierungsmethode definiert. Diese wird vom Subject im Falle von Aktualisierungen aufgerufen und ist in den meisten Fällen mit näheren Daten zur Änderung parametrisiert. Konkrete Observer implementieren das Interface und damit die Aktualisierungsmethode und bestimmen somit, wie der Observer auf die Benachrichtigung reagieren soll.
Das Subject benötigt Administrationsmethoden, damit sich Observer an- und abmelden können. Meldet sich ein Observer an, so nimmt das Subject es in seine Liste der zu benachrichtigen Objekte auf. Treten nun Änderungen am Subjectzustand auf, so werden alle registrierten Observer informiert (notifyObservers()). Dies geschieht, in dem über die Observerliste des Subjects iteriert wird, und auf jedem Observer die Aktualisierungsmethode (update()) aufgerufen wird.
ASubject:
public abstract class ASubject {
private final List<IObserver> observerList = new ArrayList<IObserver>();
public void register(IObserver pNewObserver){
observerList.add(pNewObserver);
}
public void unregister(IObserver pNewObserver){
observerList.remove(pNewObserver);
}
protected void notifyObservers(int pState){
for (IObserver observer : observerList) {
observer.update(pState);
}
}
}
ConcreteSubject mit einem int als Zustand. Wenn dieser geändert wurde (setState()) werden alle Observer benachrichtigt:
public class ConcreteSubject extends ASubject {
private int _state;
public void setState(int pState) {
_state = pState;
//Wenn das Subject die Aktualisierung selbst durchführen soll,
//alternativ kann die Methode auch vom Client aufgerufen werden.
notifyObservers(pState);
}
public int getState() {
return _state;
}
}
IObserver. Hier ist update() mit einem int parametrisiert. Der neue Zustand des Subject wird in dieser Form übergeben (push-Methode). ConcreteObserverA, ConcreteObserverB:
public interface IObserver {
public void update(int pState);
}
public class ConcreteObserverA implements IObserver {
public void update(int state) {
System.out.println("Concrete Observer A is updated with "+state);
//ggf. Modifikationen mit setState().
}
}
public class ConcreteObserverB implements IObserver {
public void update(int state) {
System.out.println("Concrete Observer B is updated with "+state);
//ggf. Modifikationen mit setState().
}
}
Beispielclient:
public class Client {
public static void main(String[] args) {
ConcreteSubject concreteSubject = new ConcreteSubject();
concreteSubject.register(new ConcreteObserverA());
concreteSubject.register(new ConcreteObserverB());
concreteSubject.setState(77);
//Concrete Observer A is updated with 77
//Concrete Observer B is updated with 77
}
}
Aufgrund der großen Beliebtheit des Observer Patterns haben sich viele Variationen ausgebildet. Dabei gibt es kein richtig und falsch, sondern nur ein zweckmäßig und nicht zweckmäßig im bestimmten Fall.
Für die Art und Weise, wie Observer die benötigten Informationen erhält, gibt es zwei Varianten.
Im Push-Modell übergibt das Subject der update()-Methode detaillierte Informationen über die Änderung als Parameter:
Aktualisierungsmethode nach dem Push-Modell:
public interface IObserver1 {
public void update(int pLength, int pWidth, boolean pVisible, String pName);
}
Der Vorteil hierbei ist, dass Observer und Subject noch stärker entkoppelt sind, da der Observer keine Informationen über das Subject benötigt. Auch ist nur ein Methoden-Aufruf zur Übergabe der Informationen nötig. Das Problem bei dieser Vorgehensweise liegt jedoch in den unterschiedlichen Bedürfnissen der Observer begründet: Nicht jeder Observer benötigt zwangsläufig alle Parameter, die ihm übergeben werden. Es kann unnötiger Datentransfer entstehen. Weiterhin werden Erweiterungen erschwert: Was ist, wenn die update()-Schnittstelle um einen weiteren Parameter erweitert werden soll, da ein Observer plötzlich mehr Informationen benötigt? Es müssen alle konkreten Observer angepasst werden. Abhilfe könnte in diesem Fall die Nutzung eines speziellen (Event-)Objektes als Paramter statt der Übergabe der einzelnden Parameter: Dieses Objekt kann alle notwendigen Daten kapseln. Weitere Updateinformationen könnten einfach dem Objekt hinzugefügt werden, ohne dass die Clients brechen. Dieses Vorgehen ist der klassische Ansatz von zahlreichen GUI-Bibliotheken (wie Swing): Eventobjekte kapseln die Änderungsinformation:
Eventobjekt als Parameter der update()-Methode am Beispiel von java.awt.event.ActionListener (weitere Information zu dieser hybriden Lösung unter AWT/Swing Eventhandling):
public interface ActionListener extends EventListener {
/**
* Invoked when an action occurs.
*/
public void actionPerformed(ActionEvent e);
}
Beim Pull-Modell erhält der Observer nur eine minimale Benachrichtigung und muss sich die benötigten Informationen selber aus dem Subject holen. Dazu erhält/besitzt es eine Referenz auf das ConcreteSubject (entweder in einer Instanzvariable beim Registrieren gespeichert oder via Argument der update()-Methode).
Aktualisierungsmethode nach dem Pull-Modell:
public interface IObserver2 {
public void update(ConcreteSubject pConcreteSubject);
}
Bekommt der Observer nun die Aktualisierungsnachricht, so holt er sich die benötigten Informationen mittels Getter vom konkreten Subject selbst. Damit ist gewährleistet, dass jeder Observer nur die Informationen erhält, die er auch wirklich benötigt. Außerdem werden problematische Situationen entschärft, in denen ein Observer mehrere gleiche Subjects beobachtet. Beim Push-Verfahren wäre unkar, von welchem Subject das Update kommt. Das Pull-Modell ermöglicht es die update()-Schnittstelle stabil zu halten: benötigt ein Observer mehr Informationen, so muss er lediglich ein Getter mehr auf dem ConcreteSubject aufrufen. Der Code der anderen Observer bleibt unangetastet. Allerdings kann das Pull-Modell ineffizient werden, da der Observer ohne Hilfe herausfinden muss, was sich konkret geändert hat.
Merke: Wenn das Subject Aussagen über die Bedürfnisse seiner Observer treffen kann (beispielsweise, wenn nur ein Observer existiert oder einige gleichartige), dann ist das Push-Modell zu bevorzugen. Weiß das Subject aber nichts über seine Observer (beispielweise, wenn es viele verschiedenartige Observer sind), dann sollte das Pull-Modell realisiert werden.
Die Subjectschnittstelle kann auch ein Interface statt einer abstrakten Klasse sein. Dadurch ist man gezwungen, den oft generischen Administrations- und Aktualisierungscode in jeder Subjectimplementation neu zu schreiben, statt den entsprechenden Code von einer abstrakten Superklasse zu erben. Obwohl dies Kohäsionsverlust bedeutet, kann solch ein Vorgehen in Fällen sinnvoll sein, in den kein allgemeiner Administrations- und Aktualisierungscode von den konkreten Subjects abstrahiert werden kann, da sie zu unterschiedlich sind.
Natürlich kann auch keine Schnittstelle für das Subject definiert werden und Observer können gleich gegen ein konkretes Subject arbeiten. Dies ist zweckmäßig, wenn das Subject nicht ausgetauscht werden muss.
Wohl überlegt sollte auch sein, in welcher Klasse die Verwaltungsmethoden (registerObserver(), unregisterObserver(), notifyOberserver() etc.) für die Observer sein sollen. Drei Möglichkeiten wären denkbar:
Das initiale Auslösen der Observerbenachrichtigung (notifyObservers()) kann sowohl vom Subject selber, als auch vom Client durchgeführt werden.
Ruft das Subject nach jeder Zustandsveränderung (beispielsweise in einem Setter) selbstständig notifyObservers() auf, so kann es nie passieren, dass es vergessen wird, wenn der Client (oder sonst wer) den Subjectzustand ändert. Allerdings kann dieses Vorgehen zu unnötigen Aktualisierungen führen, wenn mehrere Subjectzustände nacheinander geändert werden und jedes Mal alle Observer benachrichtigt werden. Soll der Client selber notifyObservers() aufrufen, so kann dies nicht passieren, da er erst nachdem er alle Subjectzustände modifiziert hat, die Methode aufrufen kann. Die Gefahr ist allerdings groß, dass dies vergessen wird.
Im Sinne einer guten Kapselung sollte die notifyObservers()-Methode entsprechend ihrem Aufrufer entweder public (Client) oder gar protected (ConcreteSubject) deklariert werden.
Frei nach [Scherer], Folie 12
Eine Änderung kann so eine ganze Änderungskette nach sich ziehen (Kaskade) oder im schlimmsten Fall zu sich rekursiv wiederholenden Aufrufen führen (Zyklen).In der Java API wird das Observer Design Pattern an zahlreichen Stellen angewandt.
Das klassische Anwendungsbeispiel in der Java-API ist das Eventhandling von AWT/Swing. Die GUI-Komponenten sind dabei die Subjects, bei den sich Listener, die Observer, registrieren können. Findet eine Userinteraktion auf der Komponente statt, so werden alle registrierten Listener benachrichtigt.
Der Aufbau wird im folgendem am Beispiel von Buttons und dem dazugehörigen Action- und ChangeListenern dargestellt.
Jeder Swing-Button erbt von AbstractButton Methoden zum An- und Abmelden von Listenern, sowie eine Methode fireActionPerformed() (bzw. fireStateChanged()) zur Benachrichtigung der entsprechenden Listener. Diese müssen die Aktualisierungsmethode actionPerformed(ActionEvent) bzw. stateChanged(ChangeEvent)) implementieren.
Die Methode fireActionPerformed(ActionEvent) aus AbstractButton:
public abstract class AbstractButton extends JComponent implements ItemSelectable,SwingConstants {
// viel Code...
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance
* is lazily created using the <code>event</code>
* parameter.
*
* @param event the <code>ActionEvent</code> object
* @see EventListenerList
*/
protected void fireActionPerformed(ActionEvent event) {
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
ActionEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ActionListener.class) {
// Lazily create the event:
if (e == null) {
String actionCommand = event.getActionCommand();
if (actionCommand == null) {
actionCommand = getActionCommand();
}
e = new ActionEvent(AbstractButton.this, ActionEvent.ACTION_PERFORMED,
actionCommand, event.getWhen(), event.getModifiers());
}
((ActionListener)listeners[i + 1]).actionPerformed(e);
}
}
}
// viel Code...
}
Interessant ist der Einsatz von EventObjects als Parameter für die Updatemethoden. So kreiert fireStateChanged() ein ChangeEvent-Objekt mit Angaben zum Event und übergibt es beim Aufruf von stateChanged(ChangeEvent) auf jeden Listener. Solche EventObjects kombinieren die Push- mit der Pullmethode: EventObjects enthalten zum einen Informationen über das Event (getActionCommand(), getWhen() und getModifiers() beim ActionEvent), zum anderen erlangt man über getSource() immer ein Handle auf die auslösende GUI-Komponente (konkretes Subject) und kann aus diesem die benötigten Informationen herausziehen oder die Komponente modifizieren (beispielsweise mit getText() und setText()).
Bei Listen in Swing dient das ListModel als Subject und JList als Observer (genauer gesagt hat JList eine innere Klasse AccessibleJList, die als Observer fungiert).
Wird ein Element in einem Listenmodell hinzugefügt oder entfernt, so wird eine Benachrichtigung (intervalAdded(), intervalRemoved() oder contentsChanged()) an die JList gesendet und diese stellt das neue Element da. Beim DefaultListModel (erweitert AbstractListModel und implementiert damit ListModel) sieht man dieses Verhalten sehr gut an der addElement(Object)-Methode, mit dem ein Element in die Liste hinzugefügt werden kann:
Die Methode addElement() von DefaultListModel löst die Benachrichtigung der Listener aus:
public class DefaultListModel extends AbstractListModel {
private Vector delegate = new Vector();
//viel Code...
/**
* Adds the specified component to the end of this list.
*
* @param obj the component to be added
* @see Vector#addElement(Object)
*/
public void addElement(Object obj) {
int index = delegate.size();
delegate.addElement(obj);
fireIntervalAdded(this, index, index);
}
//viel Code...
}
Daraufhin, wird die Methode fireIntervalAdded() der abstrakten Superklasse AbstractListModel aufgerufen und der ListDataListener in JList benachrichtigt.
Die Methode fireIntervalAdded() von AbstractListModel benachrichtigt alle Listener:
public abstract class AbstractListModel implements ListModel, Serializable {
protected EventListenerList listenerList = new EventListenerList();
//viel Code...
/**
* <code>AbstractListModel</code> subclasses must call this method
* <b>after</b>
* one or more elements are added to the model. The new elements
* are specified by a closed interval index0, index1 -- the enpoints
* are included. Note that
* index0 need not be less than or equal to index1.
*
* @param source the <code>ListModel</code> that changed, typically "this"
* @param index0 one end of the new interval
* @param index1 the other end of the new interval
* @see EventListenerList
* @see DefaultListModel
*/
protected void fireIntervalAdded(Object source, int index0, int index1) {
Object[] listeners = listenerList.getListenerList();
ListDataEvent e = null;
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ListDataListener.class) {
if (e == null) {
e = new ListDataEvent(source, ListDataEvent.INTERVAL_ADDED, index0, index1);
}
((ListDataListener)listeners[i + 1]).intervalAdded(e);
}
}
}
//viel Code...
}
Kommentare
ich bin immer wieder überrascht, welche Ausmaße Betriebsblindheit annehmen kann. Kurzum: Natürlich heißt es Pull (von \"ziehen\", die Parameter werden ja aus dem Subjekt rausgezogen... mit einer Abstimmung hat das nichts zu tun). Dankeschön für deinen Hinweis. :-)
Eine kleine Anmerkung: sollte es nicht statt \"poll\"-Modell \"pull\"-Modell heißen?
Heißt zumindest bei uns in der Vorlesung so...
lg
vielen Dank für deinen Hinweis. Du beschreibst den klassischen Ansatz, den auch in zahlreichen GUI-Bibliotheken mit den Eventobjekten gefahren wird: Das Eventobjekt kapselt alle Informationen zum Update und macht damit die Clients stabil gegen Änderungen an den übergebenen Daten. Warum ich bis jetzt an dieser Stelle noch nicht darauf eingegangen bin, ist mir schleierhaft. Jetzt ist es jedenfalls drin und ich danke dir für deinen Hinweis.
Philipp
Einen Zusatz habe ich jedoch. Sie sprechen beim Push-Model davon, dass eine Änderung aller Observer notwendig wird, wenn für einen einzelnen Observer mehr Informationen benötigt werden. Man kann auch diese Änderung kapseln, indem man man nur 2 Parameter übergibt. Erstens die source (je nach Anwendungsfall kann man sich das auch sparen) und zweitens ein Parameter-Objekt, welches alle zu übermittelnden Informationen enthält (reines Datenobjekt). Dann muss nur dieses Parameter-Objekt geändert werden, das Interface der Observer-Schnittstelle bleibt unverändert.
Aber wie gesagt, ansonsten wirklich äußerst gelungen!
\"...eine Referenz auf einmal Familie Fischer und einmal Familie Fischer hält.\"
Ansonsten muss ich sagen super erklärt. Ich studiere Informatik im dritten Semester. Unser Professor konnte das nicht so verständlich erklären. Vielen Dank
Seite: 1 -