4 Wie Entwurfsmuster Probleme lösen


[Seminarübersicht]...[Entwurfsmuster nach Gamma]...[5 Der Entwurfsmusterkatalog]

In diesem Abschnitt soll gezeigt werden, welche Teilaufgaben mit Hilfe von Entwurfsmustern bewältigt werden können. Diese Teilaspekte können jedoch schlecht isoliert gezeigt werden. Ich habe es deshalb auch nicht versucht, sondern vielmehr die Beispiele genutzt, um ein paar Muster genauer vorzustellen. Auch wenn jedes Muster es Wert wäre, besprochen zu werden, so macht es keinen Sinn, dies in drei Seminarvorträgen zu tun. Wer komplexe objektorientierte Entwürfe realisieren muß (oder will), sollte sich den Katalog unbedingt selbst einmal durchlesen !


Wer alles kann, braucht keine Entwurfsmuster !

Genau ! Denn die Strukturen in den Entwurfsmustern sind (bei allem Respekt, Herr Gamma !) nicht revolutionär, sondern sie bieten eine gute Möglichkeit, ohne Umwege zu guten Entwürfen zu gelangen.


Wie man Entwurfsmuster gebraucht:

Genau wie es keinen Algorithmus zum Entwurf von Algorithmen gibt, so gibt es kein Muster für den Einsatz von Entwurfsmustern. Es gibt praktisch nur eine Methode: Man liest (bzw. kennt) alle Muster, insbesondere deren Abschnitt über die Anwendbarkeit, und entscheidet, ob die Problemstellung und das Muster aufeinander passen. Ein hinreichendes Wissen über die Klassifizierung der Muster und ihre Beziehungen untereinander ermöglicht es natürlich, das Finden eines passenden Musters nicht dem Zufall zu überlassen.


1. Bestimmung der Klassenhierarchie und der Objektstruktur

Der naheliegendste, aber nicht zwingend beste Ansatz zur Bestimmung der Objektstruktur besteht in der Nachbildung der Problemstellung. Gamma übertreibt wie folgt:

"Sie können die Problemstellung aufschreiben, die Substantive und Verben unterstreichen und ihnen entsprechende Klassen und Operationen konstruieren."

Dieses (oder zumindest ähnliches) Vorgehen kann sich jedoch auf die Flexibilität des Entwurfes negativ auswirken. In vielen Fällen kann es sinnvoll sein, Objekte einzuführen, die keine Entsprechung in der Problemstellung haben, jedoch maßgeblich dazu beitragen, spätere Änderungen im Entwurf leicht realisieren zu können. Dies können beispielsweise Objekte sein, die Algorithmen oder verschiedenartige, aber ähnliche Objekte auf einer höheren Abstraktionsebene repräsentieren oder aber solche, die ihrerseits für die Erzeugung anderer Objekte zuständig sind.
Solche Objekte in der Struktur eines Entwurfes entstehen in der Regel erst im fortgeschrittenen Entwicklungsstadium. Da sie aber oft das Resultat der "Evolution" eines Entwurfes sind, ist es effizienter, sie bereits frühzeitig zu berücksichtigen. (Zum Beispiel durch den Gebrauch der Entwurfsmuster ...)

Beispiel:

In einem Programm zur Mitarbeiterverwaltung gibt es drei Arten von Mitarbeitern, die in drei Klassen implementiert sind. Die Struktur der Objektvariablen ist gleich, nur der Algorithmus zur Gehaltsberechnung ist verschieden.

Da die verschiedenen Mitarbeiterklassen in diversen Kontexten innerhalb des Programmes verwendet werden sollen, ist es sinnvoll, eine abstrakte Klasse einzuführen, von der man die anderen durch Vererbung ableitet. Achtung: Diese Veränderung ist nicht das Ergebnis der Verwendung von Entwurfsmustern; die Bildung von abstrakten Klassen ist in jedem komplexen objektorientierten Entwurf sinnvoll !

Erst jetzt kommen die Entwurfsmuster (bzw. eines davon) ins Spiel. Das objektbasierte Verhaltensmuster "Strategie" gibt uns in diesem Fall vor, wie wir unsere Objektstruktur flexibler machen können. Im Anwendbarkeits-Abschnitt dieses Musters lesen wir:

" (...) , wenn sich viele verwandte Klassen nur in ihrem Verhalten unterscheiden (...) "

Dies ist hier der Fall. Ob Chef oder Azubi, die Struktur des Datensatz-Objektes ist stets die gleiche. Lediglich die Berechnung des Gehaltes folgt verschiedenen Regeln. Der Strukturabschnitt dieses Musters zeigt uns, wie wir Algorithmen in Objekte kapseln:
Ein "Mitarbeiter"-Objekt erhält zusätzlich eine Referenz auf ein Strategieobjekt, welches hier durch eine abstrakte Oberklasse "Gehaltsermittler" vertreten wird. Eine Anfrage eines Klienten nach der Höhe des Gehaltes leitet das "Mitarbeiter"-Objekt an seinen "Gehaltsermittler" weiter. Hierbei übergibt es diesem eine Referenz auf sich selbst, damit der Algorithmus Zugriff auf die Objektvariablen des "Mitarbeiters" hat.

Somit ist eine Struktur entstanden, die nur noch eine "Mitarbeiter"-Klasse kennt und in der die Menge der Algorithmen zur Berechnung des Gehaltes beliebig veränderbar ist.


2. Klassenbildung, Bestimmung der Objektgranularität

Bei der Festlegung, wie fein eine Informationsstruktur in Objekte untergliedert werden soll (Objektgranularität), hat ein Entwickler die Qual der Wahl: Dem Objekt ist es "egal", es ist ein virtuelles Gebilde und kann quasi alles "sein". Natürlich ist nicht jede theoretisch denkbare Lösung sinnvoll. Diverse Muster bieten Hilfen für diesen Aspekt.

Beispiel: Fliegengewicht

(Ich verwende zur Beschreibung der Struktur des Fliegengewicht-Musters das gleiche Beispiel wie die "Gang Of Four", nämlich das eines WYSIWYG-Dokumenteditors, da ich mir kein Beispiel vorstellen kann, welches die Motivation des Fliegengewicht-Musters deutlicher zeigt.)

In diesem WYSIWYG-Editor ist es theoretisch denkbar (wenn auch selten der Fall), daß einzelne Zeichen gesondert formatiert werden sollen. Ein guter Dokumenteditor muß diese Möglichkeit auf jeden Fall berücksichtigen. In einem objektorientierten Softwareentwurf bedeutet dies, das jedes Zeichen durch ein eigenes Objekt repräsentiert wird, da es eigene Eigenschaften wie Schriftart, Größe oder Farbe besitzen kann. Diese Zeichen-Objekte sind in der Lage, sich selbst unter Berücksichtigung ihrer Eigenschaften auf dem Bildschirm (,Drucker,...) zu zeichnen.
Die Idee des Fliegengewicht-Musters besteht darin, diejenigen Informationen, die in der Regel nicht nur ein Objekt, sondern größere Mengen von ihnen betreffen, in seinen Kontext zu verlagern.
In unserem Beispiel bedeutet dies, daß ein in der Objekthierarchie des Dokumentes weiter oben stehendes Objekt (Zeile, Absatz oder allgemein: Block) die Werte der o.a. Eigenschaften (d.h. den Kontext) sowie Referenzen auf die enthaltenen Zeichen-Objekte speichert.
Doch damit nicht genug: Durch diese Struktur wird es möglich, von jedem im Dokument vorhandenen Zeichen nur ein Objekt bereitzustellen. Jede größere Struktur referenziert so die entsprechenden Zeichen-Objekte (typischerweise jeweils mehrmals) und delegiert (u.a.) das Zeichnen (unter Angabe der Eigenschaften als Parameter) an diese. Falls einzelne Zeichen besonders formatiert werden, kostet diese Aufteilung natürlich Ressourcen, da statt eines einzelnen Zeichen-Objektes zusätzlich ein Objekt benötigt wird, welches den Kontext des Zeichens speichert und das Zeichnen (etc.) delegiert. Dies sollte aber nicht der Normalfall sein, und gerade für diesen soll unsere Struktur optimiert sein.
Die folgende Abbildung zeigt den Ausschnitt einer solchen, hierarchischen Dokumentstruktur:


3. Spezifikation der Schnittstellen

Die Spezifikation der Schnittstellen (Glossar: Polymorphie, Schnittstelle, Signatur, Typ !!!) ist von zentraler Bedeutung. Die Schnittstelle ist das einzig sichtbare an einem Objekt. Nicht nur für die Arbeit im Team ist es wichtig, die Schnittstellen innerhalb der Objektstruktur sauber zu definieren.

Beispiel:

Ein Muster, bei dem die Gestaltung der Schnittstellen sehr wichtig (weil komplex !) ist, ist das Memento-Muster. Es dient dazu, den internen Zustand (=Variablenbelegung) eines Objektes festzuhalten und ggf. das Objekt später mit den festgehaltenen Daten zu belegen. Dies kann beispielsweise für die Realisierung von UNDO-Operationen notwendig sein. Nachfolgend ist eine Struktur abgebildet, die das Memento-Muster verwendet.

Der Klient möchte den Zustand des UrheberObjektes speichern, hat aber keinen Zugriff auf die Variablen, da er lediglich eine Referenz auf dieses Objekt besitzt. Um dem Klienten jedoch die Verwaltung des gespeicherten Zustandes zu ermöglichen, ohne die Kapselung zu zerstören, erweitert man die Klassenhierarchie um eine zusätzliche Klasse, das Memento. Eine Verwendung dieser Struktur läuft wie folgt ab:

  1. Der Klient fordert vom UrheberObjekt ein Memento an, indem er "ErzeugeMemento" aufruft.
  2. "ErzeugeMemento" ruft den Konstruktor des Mementos auf und übergibt diesem seinen Zustand (=Variablenbelegung).
  3. So erhält der Klient über das UrheberObjekt eine Referenz auf das fertige Memento und verwaltet dies nach Lust und Laune (evtl. auch mehrere)
  4. Der Klient kann das Objekt (auch öfter) anweisen, den Zustand eines Mementos (das der Klient nennen muß) anzunehmen, indem er "Zurücksetzen" aufruft und das Memento übergibt.
  5. Der Klient hat jederzeit die Möglichkeit, das Memento zu verwerfen, da dem UrheberObjekt das Memento nicht bekannt ist, es ist lediglich sein Erzeuger.

Mit dieser Struktur ist die Kapselung beibehalten worden, da der Klient das Memento zwar verwaltet, seine Struktur jedoch nicht kennt und so weder auf dessen Variablen zugreifen, noch dessen Methoden aufrufen kann.


Ein Erzeugungsmuster: Prototyp

Die bisher von mir vorgestellten vier Muster sind Struktur- bzw. Verhaltensmuster (Mehr zu dieser Unterteilung in Abschnitt 5). Der Vollständigkeit und ihrer Bedeutung halber möchte ich auch noch ein Erzeugungsmuster vorstellen.

Das Prototyp-Muster beschreibt, wie man den Prozeß der Objekterzeugung vom eigentlichen System entkoppeln kann, ohne eine zusätzliche Klassenhierarchie von erzeugenden Klassen aufzubauen. Stattdessen definiert man eine abstrakte Klasse, einen "Prototypen", der eine Methode enthält, die eine Kopie des entsprechenden Objektes (nicht eine Referenz auf es) zurückgibt. Jedes Klasse, welche von diesem abstrakten Prototypen abgeleitet ist, implementiert die Methode zum "Klonen" speziell für sich und macht sich somit ebenfalls zum Prototypen. Dies hat den Vorteil, daß die Menge der Prototypen (auch zur Laufzeit !) einfach zu verwalten (und zu erweitern !) ist.
Wird diese Technik von einem Framework benutzt, so kann der Nutzer dieses Frameworks, also der Entwickler, leicht die zur Verfügung stehende Klassenhierarchie erweitern, ohne eine Klassenhierarchie von erzeugenden Klassen hinzufügen zu müssen. Ein gutes Beispiel bieten hier Frameworks zur Entwicklung von Zeichenanwendungen. Die Zeichenelemente (elektronische Bausteine in einer Anwendung zum Schaltkreisentwurf, Piktogramme für Möbelstücke in einer "Inneneinrichtungs-Anwendung", ...) werden hier durch die entsprechenden Prototypen implementiert. So können auch Bibliotheken von Zeichenelementen angelegt werden. Nachstehend ist die allgemeine Struktur des Prototyp-Musters gezeigt. Zu beachten ist hier, daß der Klient Referenzen zu allen Prototypen (nicht nur zu deren abstrakter Klasse) unterhalten muß, um mit diesen arbeiten zu können.


[Seminarübersicht]...[Entwurfsmuster nach Gamma]...[5 Der Entwurfsmusterkatalog]...[Seitenanfang]