design-patterns-observer.html 13.6 KB
Newer Older
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
<p>Das Beobachter-Muster (engl. <i>Observer</i> oder <i>Listener</i>) wird eingesetzt, wenn mehrere Objekte dauerhaft den Zustand eines anderen Objekts beobachten. Wenn sich der Zustand des beobachteten Objekts ändert, soll dieses alle seine Beobachter darüber informieren, damit diese sich ihrerseits reaktiv aktualisieren können. Das Muster entspricht also dem Prinzip: <i>"Don't call us, we'll call you!"</i></p>

<p>Wir können uns folgendes Beispiel vorstellen: Das beobachtete Objekt ist ein Feld zur Datumsauswahl. Dessen Beobachter sind eine Aufgaben- und eine Terminliste, die ihren jeweiligen Zustand (also die Aufgaben und Termine, die anzuzeigen sind) in Abhängigkeit des ausgewählten Datums ändern.</p>
<img src="media/patterns_observer_datepicker.png" style="width:500px">
<label>Beispiel für Beobachter-Muster</label>

<p>Das beobachtete Objekt (engl. <i>Observable</i>) speichert häufig Daten in Form einer <code>List&lt;T&gt;</code> oder einer <code>Map&lt;T,T&gt;</code>, die im User Interface (UI) an verschiedenen Stellen mittels unterschiedlicher Sichten (engl. <i>Views</i>) dargestellt werden, z.B. als Tabelle oder Chart. Diese Sichten im UI sind dann die Beobachter, die ihre Darstellung bei einer Zustandsänderung des Observable aktualisieren müssen. Das grundsätzliche Ziel des Beobachter-Musters ist es also, Observable und Observer voneinander zu entkoppeln, was in der Anwendung häufig der Trennung von Modell und beobachtenden Sichten entspricht.</p>

<p>Die Abhängigkeit zwischen den Objekten kann im Software-Entwurf über unterschiedliche prinzipielle Wege abgebildet werden:<ul>
<li><b>Pull</b>: Die abhängigen Objekte sind aktiv und fragen den Zustand des beobachteten Objekts an. Wenn diese Zustandsanfrage gezielt durch eine bestimmte Interaktion des Anwenders (z.B. Öffnen einer neuen Seite oder eines neuen Dialogs) ausgelöst wird, ist das Pull-Prinzip sinnvoll. Das HTTP-Protokoll funktioniert ausschließlich nach dem Pull-Prinzip. Weniger geeignet ist das Pull-Prinzip, wenn sich der Zustand des Observable häufig ändert und die Beobachter stets den aktuellen Zustand benötigen. Da die Observer in diesem Fall nicht wissen, wann sich der Zustand des Observable ändert, müssen sie in kurzen, regelmäßigen Intervallen nachfragen, was <i>Polling</i> genannt wird.</li>
<li><b>Push</b>: Das beobachtete Objekt ist aktiv und benachrichtigt direkt und unaufgefordert seine Beobachter, indem es deren entsprechende Methoden aufruft. Das ist einfach und effizient, hat aber zur Bedingung, dass die zu informierenden Objekte vorab bekannt sind und sich zur Laufzeit nicht ändern. Da diese beiden Bedingungen in den meisten Anwendungsfällen nicht gelten, werden Pull- und Push-Prinzip zu einem bidirektionalen Kommunikationsmuster kombiniert: <i>Publish-Subscribe</i>.</li>
<li><b>Publish-Subscribe</b>: Die grundlegende Idee ist, die Abhängigkeit dadurch umzukehren, dass interessierte Beobachter ihren Wunsch, bei Zustandsänderungen benachrichtigt zu werden, beim Observable registrieren (<i>Subscribe</i>). Alle registrierten Beobachter werden durch das Observable informiert, wenn es zu einer Änderung seines Zustands kommt (<i>Publish</i>).</li>
</ul></p>

<p>Das Beobachter-Muster wird demzufolge auch <i>Publish-Subscribe</i>-Muster genannt. Die Vorstellung hierbei entspricht einem Newsletter- oder Newsfeed-Abonnement: Beobachter abonnieren Nachrichten von Herausgebern, die diese gelegentlich veröffentlichen. Das Observable ist dabei der sendende <i>Publisher</i>, und die Observer sind die empfangenden <i>Subscriber</i>, die ihr Abonnement auch wieder kündigen können, falls sie sich nicht mehr für die Nachrichten interessieren.</p>

<p>Das folgende UML-Klassendiagramm visualisiert das Beobachter-Muster.</p>
<img src="media/patterns_observer.png" style="width:800px">
<label>Entwurfsmuster Beobachter</label>

<p>Ein Observable bietet Methoden an, um Beobachter zu registrieren (<code>addObserver</code>) und zu entfernen (<code>deleteObserver</code>) und verwaltet seine Beobachter in einer entsprechenden Liste (<code>observers</code>). Das folgende UML-Sequenzdiagramm verdeutlicht den grundlegenden Ablauf des Beobachter-Musters. Sobald der Zustand des konkreten Observable von außen geändert wird (<code>setState</code>), informiert das Observable über die Methode <code>notifyObservers</code> alle registrierten konkreten Beobachter, indem es deren Methode <code>update</code> aufruft und den neuen Zustand sowie sich selbst als Quelle dieses neuen Zustands weitergibt.</p> 

<p>In einer alternativen Variante des Musters kann die Methode <code>update</code> auch ohne Argumente dargestellt werden. In diesem Fall muss ein Beobachter nach der Benachrichtigung sich den neuen Zustand selbst über eine entsprechende Methode <code>getState</code> vom Observable abholen.</p>

<img src="media/patterns_observer_seq.png" style="width:700px">
<label>Sequenzdiagramm für das Entwurfsmuster Beobachter</label>

<p>Zur Anwendung des ursprünglichen Beobachter-Musters werden in Java <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Observable.html"><code>java.util.Observable</code></a> und <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Observer.html"><code>java.util.Observer</code></a> bereitgestellt. Jedoch sind beide seit Java 9 als <i>deprecated</i> gekennzeichnet und daher nicht weiter zu empfehlen. Das folgende Code-Beispiel demonstriert die Verwendung von <code>Observable</code> (Zeile 1) und <code>Observer</code> (Zeile 11), wobei <code>Observer</code> ein funktionales Interface (engl. <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/function/package-summary.html"><i>Functional Interface</i></a>) ist, d.h. das Interface schreibt genau eine Methode vor und kann daher in Lambda-Ausdrücken implementiert werden.</p>

<p>Alle im Folgenden gezeigten Code-Beispiele zum Entwurfsmuster Beobachter finden sich im Verzeichnis <a href="/patterns/observer" class="repo-link">/patterns/observer</a> des Modul-Repository.</p>

<pre><code class="language-java line-numbers">class MyObservable extends Observable {
    Object state;

    void setState(Object newState) {
        this.setChanged();
        this.notifyObservers(state = newState);
    }

    public static void main(String[] args) {
        // Observer is implemented as a lambda expression
        Observer o1 = (source, newState) -> System.out.printf("Received new state = %s from %s\n", newState, source);

        MyObservable source = new MyObservable();
        source.addObserver(o1);

        List.of(1, 2, 3).forEach(i -> source.setState(i));
    }
}</code></pre>

51
<p>Das folgende Code-Beispiel demonstriert die Verwendung eines <code>PropertyChangeListener</code> als neuere Alternative zu <code>java.util.Observer</code>. Anstatt <code>java.util.Observable</code> zu erweitern, kann die Liste der registrierten Beobachter und die Methode <code>notifyObservers</code> auch eigenständig implementiert werden (Zeilen 3-7). Die Beobachter können in diesem Fall das funktionale Interface <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/java/beans/PropertyChangeListener.html"><code>java.beans.PropertyChangeListener</code></a> implementieren und über dessen Methode ein <code>PropertyChangeEvent</code> empfangen (Zeilen 15-17). </p>
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

<pre><code class="language-java line-numbers">class MySecondObservable {
    Object state;
    List&lt;PropertyChangeListener&gt; listeners = new ArrayList&lt;&gt;();

    void notifyListeners(String property, Object oldValue, Object newValue) {
        listeners.forEach(l -> l.propertyChange(new PropertyChangeEvent(this, property, oldValue, newValue)));
    }

    void setState(Object newState) {
        notifyListeners("state", state, state = newState);
    }

    public static void main(String[] args) {
        // Observer is implemented as a lambda expression
        PropertyChangeListener o1 = (PropertyChangeEvent e) ->
68
69
            System.out.printf("Received new state = %s [previous state = %s] from %s\n", 
                e.getNewValue(), e.getOldValue(), e.getSource());
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

        MySecondObservable source = new MySecondObservable();
        source.listeners.add(o1);

        List.of(1, 2, 3).forEach(i -> source.setState(i));
    }
}
</code></pre>

<p>Der <code>PropertyChangeListener</code> ist ein spezieller <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/EventListener.html"><code>java.util.EventListener</code></a>. In Java begegnen uns bei der UI-Programmierung häufig spezielle <code>EventListener</code> als Teil des Beobachter-Musters, um z.B. verschiedene UI-Komponenten wie Textfelder, Buttons u.ä. oder die Interaktion des Anwenders über Tastatur, Maus u.ä. zu beobachten. In JavaFX wird üblicherweise das Interface <a href="https://openjfx.io/javadoc/11/javafx.base/javafx/event/EventHandler.html"><code>javafx.event.EventHandler</code></a>, das auch eine <code>EventListener</code>-Spezialisierung ist, verwendet, um ein anderes Objekt zu beobachten und auf dessen Zustandsänderung zu reagieren:</p>
<pre><code class="language-java line-numbers">interface EventHandler&lt;T extends Event&gt; extends EventListener {
    void handle​(T event);
}
</code></pre>

<p>Der folgende Code zeigt, wie in JavaFX die Tastatureingabe, die Mausbewegung und ein Button beobachtet werden können. Dabei sind <code>Button</code> und <code>Scene</code> durch JavaFX bereitgestellte Observables.</p>
<pre><code class="language-java line-numbers">Scene scene = new Scene(/* ... */);
scene.setOnKeyPressed((KeyEvent e) -> System.out.println("Pressed key: " + e.getCode()));

Button btn = new Button(/* ... */);
btn.setOnMouseEntered((MouseEvent e) -> System.out.println("Moved mouse over button"));
btn.setOnAction((ActionEvent e) -> System.out.println("Clicked button"));
</code></pre>

<p>Die seit Java 9 neue Klasse <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.html"><code>java.util.concurrent.Flow</code></a> stellt die Interfaces <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.Publisher.html"><code>Publisher</code></a> und <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Flow.Subscriber.html"><code>Subscriber</code></a> bereit, über die das Beobachter-Muster als asynchroner Datenstrom (engl. <i>Stream</i>) realisiert werden kann. Der folgende Code demonstriert eine grundlegende Verwendung der Flow-API.</p>
<pre><code class="language-java line-numbers">class MySubscriber implements Subscriber&lt;Integer&gt; {
    Subscription subscription;

    @Override
    public void onSubscribe(Subscription subscription) {
        System.out.println("Subscription startet");
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(Integer item) {
        System.out.printf("Received new state = %s\n", item);
        this.subscription.request(1);
    }

    @Override
    public void onError(Throwable throwable) { }

    @Override
    public void onComplete() { System.out.println("Subscription ended"); }
}
</code></pre>

<p>Ein konkreter Subscriber muss die Methoden <code>onSubscribe</code>, <code>onNext</code>, <code>onError</code> und <code>onComplete</code> implementieren. Beim initialen Aufruf von <code>onSubscribe</code> (Zeile 5) erhält der Subscriber ein Objekt vom Typ <code>Subscription</code>, über das er steuern kann, ob er in der Folge weitere Nachrichten erhalten möchte (Methode <code>request</code> ist auszuführen, Zeilen 8+14) oder nicht (Methode <code>cancel</code>). Falls ein Subscriber weitere Nachrichten erhalten möchte, werden ihm diese über die Methode <code>onNext</code> (Zeile 12) zugestellt.</p> 

<pre><code class="language-java line-numbers">class MyPublisher {
    public static void main(String[] args) throws InterruptedException {
        SubmissionPublisher source = new SubmissionPublisher&lt;Integer&gt;();
        source.subscribe(new MySubscriber());

        List.of(1, 2, 3).forEach(i -> source.submit(i));
        source.close();
        Thread.sleep(100); // wait for termination of subscriber thread
    }
}
</code></pre>

<p>Die Klasse <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/SubmissionPublisher.html"><code>SubmissionPublisher</code></a> bietet eine Standard-Implementierung des <code>Publisher</code>-Interface. Nachrichten, die über die Methode <code>submit</code> (Zeile 6) eingereicht werden, sendet der Publisher an alle aktuell registrierten Subscriber, bis er über die Methode <code>close</code> (Zeile 7) geschlossen wird. Hervorzuheben ist, dass ein Publisher die Nachrichten asynchron zustellt, d.h. er wartet nicht auf eine Empfangs- oder Verarbeitungsbestätigung der Subscriber, die ihrerseits jeweils in einem parallelen Thread (s. Kapitel <a href="#unit-threads" class="navigate">Threads in Java</a>) ablaufen. Jeder aktuelle Subscriber erhält neu übermittelte Nachrichten in derselben Reihenfolge, wie sie versendet werden - außer bei Exceptions (<code>onError</code>) oder bei Timeouts. Wenn eine Nachricht über <code>offer</code> anstatt <code>submit</code> versendet wird, kann ein Timeout definiert werden, falls ein Subscriber zu lange braucht, um die Nachricht entgegenzunehmen. Auf diese Weise können Publisher als nicht blockierende, <a href="http://www.reactive-streams.org/">reaktive Streams</a> fungieren. Damit der Subscriber-Thread ausreichend Zeit zur Verarbeitung der Nachrichten hat, schläft der Main-Thread im obigen Code eine Weile (Zeile 8). Das ist für die Praxis keine gute Lösung sondern ein <a href="https://de.wikipedia.org/wiki/Anti-Pattern">Anti-Pattern</a>, aber es trägt hier zur Erhaltung der Einfachheit des Beispiels bei.</p>