Patterns

Patterns anwenden

generelle Vorgehensweise

Die prinzipielle Idee hinter den Patterns ist ja, diese zu katalogisieren, damit man, nachdem man ein Problem identifiziert hat, einfach eine Lösung nachschlagen kann. Das funktioniert auch soweit ganz gut. Wichtig ist nur, dass man das Problem erkennt und sich daran erinnert, dass es dafür vielleicht ein Pattern geben könnte. Es ist also nicht nötig, alle möglichen Patterns auswendig zu kennen. Vielmehr sollte man einen groben Überblick über die wichtigsten Patterns haben, damit man sich daran erinnern und dann auch wirklich nachschlagen kann. „Ah, dafür gabs doch irgend so ein Pattern.“

Nach dem Finden des betreffenden Patterns muss man sich überlegen, ob es wirklich zum konkreten Problem passt — wie gesagt ist das falsche oder unnötige Verwenden von Mustern kontraproduktiv — und es danach ggf. variieren und schließlich anwenden. Letzteres hängt ein bisschen vom jeweiligen Patterntyp ab. Architekturmuster wendet man anders an, als Entwurfsmuster. Meistens ist relativ klar, was zu tun ist.

Beispiel: Das Observer-Pattern

Im Folgenden sehen wir uns das Observer-Pattern (GoF:293) als Beispiel an. Wir stellen uns dafür folgende Situation vor: Angenommen, wir wollen einen kleinen Chat bauen, mit dem man sich über TCP-Verbindungen im LAN unterhalten kann. Dazu brauchen wir zumindest (wir wollen das Beispiel möglichst einfach halten) eine einfache GUI, eine Klasse ChatClient, die die Chatnachrichten sendet und empfängt, und eine Klasse ChatServer, die die Chatnachrichten vom den Clients entgegen nimmt und an die jeweils anderen weiterleitet. So weit, so einfach.

Interessant wird es nun, wenn ein Client auch Nachrichten empfangen können soll (was für einen Chat ja ganz sinnvoll wäre). Die Daten müssen dabei irgendwie in die GUI gelangen, damit der User auch sieht, was ihm geschrieben wurde. Eine Möglichkeit wäre, die ChatClient-Klasse direkt die Daten in die GUI eintragen zu lassen. Das funktioniert, ist aber sehr unschön. Es koppelt den ChatClient an eine ganz spezielle GUI, was die Wiederverwendung beträchtlich erschwert. Außerdem muss bei Änderung der GUI die ChatClient-Klasse angepasst werden, obwohl diese ja eigentlich gar nicht betroffen sein sollte.

Damit also Wiederverwendung und Wartbarkeit nicht unnötig leiden, müssen GUI und Chat entkoppelt werden. Normalerweise greift die GUI auf die Applikationsklassen zu, aber Zugriffe auf die GUI von außen (bzw. von unten, wenn wir uns unser kleines Chatsystem in Schichten denken) sind strikt verboten. Die einfachste Lösung dieses Problems wäre, die GUI pollen zu lassen, d.h. die GUI würde in regelmäßigen Abständen beim ChatClient nachfragen, ob neue Nachrichten vorhanden sind. Das geht und es löst die oben genannten Probleme. Jedoch kann es als unbefriedigend angesehen werden, dass andauernd nachgefragt wird, obwohl die Information, dass es keine neuen Nachrichten gibt, eigentlich schon im System vorhanden ist. Zudem ist Polling aus Performancegründen nicht so gut, was uns in unseren kleinem kleinen Beispiel keine größeren Probleme machen sollte, jedoch in der Praxis natürlich schon relevant sein kann.

Typischerweise werden in solchen Fällen deshalb Eventmechanismen eingesetzt. In Sprachen wie C# gibt es schon eingebaute Sprachfeatures die so etwas unterstützen (z.B. Multicast-Delegates). Diese erlauben es, einem Event ein oder mehrere Methoden zuzuordnen, die bei Auftreten des Ereignisses automatisch aufgerufen werden. In Delphi gibt es zumindest noch Callback-Methoden, die es zumindest erlauben eine Methode bei einem Ereignis aufzurufen, was oftmals ausreichend ist. In manchen Sprachen — wie etwa in Java — gibt es solche Sprachfeatures nicht. Aber auch hier lassen sich Eventmechanismen umsetzen.

Eventmechanismen? Da gabs doch… genau: ein Pattern. Wir schlagen nach und finden das Observer-Pattern. Jetzt müssen wir nur noch sehen, ob es für unsere Zwecke auch geeignet ist. Im GoF-Book heißt es dazu: „Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.“ In unseren Fall muss nur die GUI benachrichtigt werden, eine „one-to-many dependency“ ist hier also eigentlich nicht nötig, aber die Sache mit der Benachrichtigung passt wunderbar. Des weiteren listet das GoF-Book unter „Applicability“ drei Fälle auf, in denen man das Muster verwenden kann. Der dritte Punkt beschreibt genau das, was wir tun wollen: „When objects should be able to notify other objects without making assumptions about who these other objects are. In other words, you don’t want these objects tightly coupled.“ Wir können das Observer-Pattern also für unsere Zwecke nutzen.

Jetzt stellt sich natürlich noch die Frage, ob es einen einfacheren Weg gibt und in der Tat gibt es, wie erwähnt, in manchen Sprachen einfachere Wege. Stellen wir uns nun vor, wir wollten unsere Software in Java schreiben. Hier haben wir also keine einfachere Möglichkeit und zudem wissen wir vielleicht, dass auch andere vor uns schon das Observer-Pattern für solche Zwecke in Java eingesetzt haben. Dem Einsatz steht jetzt also nichts mehr im Wege.

Kollaborationen und Varianten des Musters

Schauen wir uns nun das Muster etwas genauer an. Entwurfsmuster definieren meist gewisse Rollen. Diese werden zwar als Klassen dargestellt, man sollte aber nicht vergessen, dass es sich wirklich jeweils nur um Rollen handelt. Normalerweise legt man bei der Anwendung eines Patterns keine neuen Klassen an, sondern mappt die einzelnen Rollen auf bestehende Klassen. Manchmal muss man natürlich trotzdem neue „künstliche“ Klassen einführen, aber zuerst sollte man mal versuchen, die Rollen einfach auf bestehende Klassen zu mappen.

Die UML bietet mittlerweile auch eine Möglichkeit, ein solches Mapping direkt im Diagramm kenntlich zu machen: die Kollaboration (nicht zu verwechseln mit den Kollaborationsdiagrammen, die aus diesem Grund mittlerweile Kommunikationsdiagramme heißen). Möchte man ein Pattern beschreiben, so kann man eine Kollaboration definieren, indem man einen gestrichelten Kreis um die Klassen, die die Rollen darstellen, zeichnet. Das Observer-Pattern wie es im GoF-Book beschrieben ist, sähe dann folgendermaßen aus:

ObserverPattern nach GoF

Normalerweise dokumentiert man das Observer-Pattern nicht selbst, sondern geht davon aus, dass es allgemein bekannt ist. Bei komplexen oder weniger bekannten Mustern, Abwandlungen von Mustern oder Kollaborationen, die (noch) keine Muster darstellen, kann ein solches Diagramm aber durchaus in Entwicklerdokumentationen zu finden sein.

Nun fragen wir uns, ob wir das Muster direkt so übernehmen können oder noch abwandeln müssen. Das GoF-Book selbst beschreibt dazu schon mehrere Varianten. Neben einer zusätzlichen ChangeManager-Klasse, und anderen Ansätzen, die für uns nicht relevant sind, wird außerdem zwischen push- und pull-Observern unterschieden. Beim Pull-Verfahren werden die Observer nur benachrichtigt und holen sich dann alle nötigen Informationen selbst, wohingegen beim push-Verfahren schon Informationen über die Art der Statusänderung in Parametern mitgeliefert werden. Je nachdem wie viele Informationen direkt mitgegeben werden und wie viele sich der Observer selbst holen muss, gibt es natürlich auch verschiedene Zwischenformen. Das Diagramm zeigt das pull-Verfahren, was man an den fehlenden Parametern der update()-Methode erkennt.

Beide Varianten haben ihre Vor- und Nachteile. Das pull-Modell entkoppelt etwas stärker, da das Subject nicht wissen muss für was sich potenzielle Observer interessieren könnten. Dafür müssen die Observer aber jeweils immer alle relevanten Daten lesen und ggf. herausfinden, was sich nun konkret geändert hat. Beim push-Modell ist die logische Kopplung etwas höher, jedoch ist diese Herangehensweise etwas performanter. Zudem hat das push-Modell noch den Vorteil, dass man die update()-Methode passend benennen kann bzw. einen Observer auch mit mehreren unterschiedlichen update()-Methoden (ggf. mit unterschiedlichen Parametern) versehen kann, sodass dieser leicht auf verschiedene Arten von Events unterschiedlich reagieren kann. Die passenderen Bezeichner fördern zudem die Lesbarkeit.

Bei der Auswahl der Variante können zudem programmiersprachliche „Gepflogenheiten“ eine Rolle spielen. So ist es in Java üblich, die folgende Observer-Variante einzusetzen:

Die Listener-Variante des Observer-Patterns

  • Subject und ConcreteSubject werden nicht unterschieden.
  • Observer heißen Listener.
  • Die update()-Methoden — es kann mehrere geben — haben Bezeichner, die das eingetretene Ereignis benennen.
  • Es wird eine Kombination aus dem push- und dem pull-Verfahren verwendet. Einerseits enthalten die update()-Methoden schon Parameter mit einigen Informationen über das Ereignis. Andererseits enthalten sie auch eine source-Referenz, über die auf das Subject zugegriffen werden kann.
  • Die Informationen über das Event werden nicht direkt als Parameter übergeben, sondern in einer Event-Klasse gekapselt. Das sorgt für ein stabileres Interface.
  • Dadurch, dass die source-Referenz im Event steht und nicht im Observer hardgecodet ist, kann ein Listener mehreren Subjects zugeordnet sein.

Wir entscheiden und hier für die in Java übliche Listener-Variante des Observer-Patterns.

Wenn man nun die Kollaboration (die ein Pattern sein kann, aber nicht muss) anwenden will, kann man das beispielsweise folgendermaßen tun:

Anwendung des Listener-Patterns

Wenn man die Kollaboration anwenden möchte, werden einfach die jeweiligen Rollen genannt und mit der Kollaboration verbunden. Es wäre dabei noch möglich, gewisse Methoden, die das Muster definiert (beispielsweise addListener() bzw. hier eher addChatClientListener()), mit in das Diagramm aufzunehmen. Das ist aber nicht unbedingt notwendig, da diese Information ja schon im Muster definiert ist. Ähnlich ist es mit den Assoziationen und Abhängigkeiten. Man sollte hier jeweils für den konkreten Fall überlegen, was sinnvoll ist.

Bei der Anwendung von Patterns ist es nicht ungewöhnlich, dass man mehrere Rollen auf die selbe Klasse mappt oder eine Rolle mehreren Klassen zuweist. Auch andere Variationen von Mustern sind nicht ungewöhnlich. Patterns sind nicht in Stein gemeißelte Gesetze, sondern sollen nützliche Hilfen sein. In unserem Beispiel hier haben wir uns der Einfachheit halber entschieden, keine Event-Klasse zu verwenden, sondern die Daten (hier: die Chat-Nachricht) direkt als Parameter zu übergeben. Das ist einfacher, erzeugt aber weniger stabile Interfaces, d.h. es könnte passieren, dass wir zusätzliche Parameter einführen müssen und dadurch Code ändern müssen, der eigentlich gar nicht betroffen sein solle. Auch das ist wieder eine Abwägung, die getroffen werden muss und stark vom gegebenen Kontext abhängt.

Kapitel: | Zurück | 1 | 2 | 3 | 4 | 5 | 6 | Weiter |

3 Kommentare


  1. > _In_ meinen Augen vermischen diese Definitionen jedoch oft

    Typo.


  2. > Außerdem muss bei Änderung der GUI die _ChatClient_-Klasse angepasst werden

    Typo.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert