18. März 2009
Design Patterns sind wiederverwendbare Miniaturmodelle, die das Entwickeln von Software-Architekturen vereinfachen sollen. Obwohl verschiedene Programme in ihrer Gesamtheit unterschiedliche Anforderungen erfüllen müssen, müssen interne Module zum Teil ähnliche Probleme lösen. Anstatt das Rad in der Modellierung immer wieder neu zu erfinden, können Design Patterns wiederverwendet werden. Auch für den Fall, dass sie nicht exakt zu den Anforderungen passen, die ein Modell erfüllen muss, bieten sie immerhin Denkanstöße und helfen Entwicklern so nachzuvollziehen, wie andere Entwickler bestimmte Aufgabenstellungen gelöst haben.
Dieser Inhalt ist unter einer Creative Commons-Lizenz lizensiert.
Design Patterns, im Deutschen Entwurfsmuster, sind Miniaturmodelle, die für bestimmte Situationen wiederverwendet werden können. Sie sollen Entwicklern helfen, objektorientierte Programme besser zu strukturieren, weil sie wiederverwendbare Bausteine für die Modellierung darstellen.
Einer der ersten Schritte, wenn ein Programm entwickelt oder ein bestehendes Programm erweitert werden soll, ist zu überlegen, welche Struktur zu entwerfen ist. Bei in C++ entwickelten Programmen sind dabei typischerweise Klassen und ihre Beziehungen gemeint. Auf der einen Seite soll die Struktur einfach, überschaubar und möglichst greifbar sein. Auf der anderen Seite muss sie aber auch flexibel sein, da sich Anforderungen ändern können und ein Programm möglichst einfach angepasst werden können muss.
Design Patterns sind der Versuch, für bestimmte Situationen Modelle vorzuschlagen, die ein Entwickler im Gesamtmodell seiner Software wiederverwenden kann. Indem man Design Patterns studiert und versteht, wie sie funktionieren, wann sie eingesetzt werden können und welche Vorteile sie bieten, kann die Struktur einer Software verbessert werden.
In diesem Artikel sollen einige Design Patterns näher vorgestellt und mit einfachen C++-Beispielen erläutert werden.
Erzeugungsmuster basieren auf sogenannten Fabriken, deren Aufgabe es ist, Objekte zu erzeugen. Sie werden im Englischen factory patterns genannt.
Um Objekte zu erzeugen - sprich Klassen zu instantiieren - muss man in C++ lediglich das Schlüsselwort new
verwenden. So wird im folgenden Beispiel angegeben, dass eine Variable vom Typ int
im Heap angelegt werden soll.
int *i = new int;
Wenn auf das Schlüsselwort new
zugegriffen wird, passieren genaugenommen zwei Dinge: Der Programmierer sagt zum einen, was er haben will - also zum Beispiel ein int
. Und er sagt zum anderen, wie es erstellt werden soll - bei new
im Heap.
Erzeugungsmuster weisen nun die Verantwortung für das Wie einer anderen Funktion oder Klasse zu. Der Programmierer sagt nur mehr, was er haben will - das Wie ist innerhalb einer anderen Funktion oder Klasse implementiert.
Ein sehr einfaches Erzeugungsmuster ist die Fabrikmethode. Anstatt ein Objekt mit new
zu instantiieren wird auf eine Fabrik zugegriffen, die die Erzeugung vornimmt.
class int_fabrik { public: int *erzeuge() { return new int; } }; int main() { int_fabrik fabrik; int *i = fabrik.erzeuge(); }
Die Klasse int_fabrik
wird innerhalb der Funktion main()
verwendet, um eine Variable vom Typ int
zu erzeugen. Indem auf die Klasse int_fabrik
zugegriffen wird, ist klar, was der Programmierer haben will. Wie Variablen vom Typ int
aber genau erzeugt werden, bestimmt allein die Klasse int_fabrik
.
Es gibt auch Fabriken, die unterschiedliche Arten von Objekten erzeugen können. Dazu müssen die Klassen, auf denen die Objekte basieren, aber in einer Hierarchie stehen.
#include <iostreams> #include <string> class fabrik; class fliegend { public: virtual void fliegen() = 0; }; class vogel : public fliegend { public: void fliegen() { std::cout << "Pieppiep" << std::endl; } private: vogel(); friend class fabrik; }; class biene : public fliegend { public: void fliegen() { std::cout << "Summsumm" << std::endl; } private: biene(); friend class fabrik; }; class fabrik { public: fliegend *erzeuge(std::string s) { if (s == "vogel") return new vogel; else if (s == "biene") return new biene; else return 0; } }; int main() { fabrik fab; fliegend *f = fab.erzeuge("vogel"); f->fliegen(); }
Im obigen Programm sind die beiden Klassen vogel
und biene
von fliegend
abgeleitet. Während die Klasse fliegend
abstrakt ist, da sie eine rein virtuelle Methode enthält, können von den Kindklassen vogel
und biene
Instanzen erstellt werden. Dazu kann die Klasse fabrik
verwendet werden, die eine Methode erzeuge()
anbietet. Wenn diese Methode aufgerufen wird, muss ihr ein Parameter vom Typ std::string
übergeben werden, der angibt, welches Objekt genau erstellt und zurückgegeben werden soll. Damit die Klasse fabrik
nicht umgangen werden kann, sind die Konstruktoren der Klassen vogel
und biene
außerdem privat. Damit die Fabrik aber auf den Konstruktor zugreifen kann und Klassen instantiieren kann, ist sie als friend
deklariert.
Das Verstecken der Objekterzeugung in eine eigene Klasse hat den Vorteil, dass an zentraler Stelle der Code angepasst werden kann, wenn zum Beispiel die Objekterzeugung auf eine andere Art und Weise stattfinden soll. So könnte zum Beispiel im obigen Programm die Klasse vogel
durch eine andere ersetzt werden, ohne dass der Code in der Funktion main
geändert werden muss. Die neue Klasse müsste lediglich von fliegend
abgeleitet sein und in der Methode erzeuge()
der Klasse fabrik
instantiiert und zurückgegeben werden.
Auch dann, wenn zum Beispiel sehr viele Objekte in einem Programm erzeugt werden müssen, könnte die Fabrik für eine schnellere dynamische Speicherallokation angepasst werden. Die Klasse fabrik
würde dann einen großen Speicherblock vom Betriebssystem anfordern und dann Objekte nach und nach im Speicherblock anlegen, ohne für jedes einzelne Objekt das Betriebssystem mit new
um ein bisschen neuen Speicher zu bitten. Auch diese Anpassung könnte erfolgen, ohne dass im restlichen Programm irgendwelche Änderungen vorgenommen werden müssen. Lediglich die Klasse fabrik
müsste angepasst werden.
Es gibt verschiedene Spielarten von Erzeugungsmustern, die sich alle sehr ähnlich sehen. So könnte zum Beispiel in C++ anstatt einer Klasse auch eine Fabrikfunktion verwendet werden. Dank Templates könnte sie wie folgt aussehen.
template <typename T> T *erzeuge() { return new T; } int main() { int *i = erzeuge<int>(); }
Auch hier gilt: Das Was und Wie sind getrennt. Das Template könnte jederzeit angepasst oder für verschiedene Datentypen spezialisiert werden - der Rest des Programms wie die Funktion main()
im obigen Beispiel müsste nicht angepasst werden.
Abschließend soll noch ein spezielles Erzeugungsmuster vorgestellt werden, das Singleton genannt wird. Dahinter verbirgt sich eine Fabrik, die nur ein einziges Objekt erzeugen kann. Wird auf die Fabrik ein zweites Mal zugegriffen, gibt sie genau das gleiche Objekt zurück, das sie beim ersten Zugriff erstellt hat.
int *erzeuge_ein_int() { static int i; return &i; } int main() { int *i = erzeuge_ein_int(); int *j = erzeuge_ein_int(); }
Das Entwurfsmuster bedient sich im obigen Beispiel einer einfachen Funktion namens erzeuge_ein_int()
. Diese Funktion erstellt nicht nur eine Variable vom Typ int
. Sie erstellt auch nur genau eine Variable, da bei jedem weiteren Aufruf die gleiche Adresse zurückgegeben wird. Die beiden Zeiger i und j speichern also die gleiche Adresse.
Selbstverständlich wäre es bei dem oben gezeigten Beispielprogrammen weiterhin möglich, weitere Variablen vom Typ int
per new
zu erzeugen. Erzeugungsmuster werden im Allgemeinen auch nicht im Zusammenhang mit primiviten Datentypen verwendet, sondern mit Klassen. Indem man wie im vorherigen Beispiel mit der abstrakten Klasse fliegend
Konstruktoren privat macht, kann man verhindern, dass ein Entwickler eine Fabrik umgeht und mit new
direkt eine Instanz der Klasse erzeugt.
Ein prominenter Vertreter des Erzeugungsmusters ist die Windows-Funktion CoCreateInstance()
. Diese Funktion kann in einem Windows-Programm aufgerufen werden, um ein sogenanntes COM-Objekt zu erzeugen. Der Funktion CoCreateInstance()
werden dabei verschiedene Parameter übergeben, die das COM-Objekt näher spezifizieren. So besitzt zum Beispiel jedes COM-Objekt eine eindeutige Identifikationsnummer. Wenn CoCreateInstance()
aufgerufen wird, wird die Identifikationsnummer des COM-Objekts angegeben, das man haben möchte. Die Funktion selbst kümmert sich dann darum, das Objekt zu erzeugen. Dabei wird unter anderem die Windows-Registry nach der Identifikationsnummer durchsucht, um die DLL-Datei zu finden, in der sich das COM-Objekt befindet. Da COM sogar das Erzeugen von Objekten auf anderen Computern im Netzwerk unterstützt, macht die Fabrikfunktion wesentlich mehr als in den hier vorgestellten einfachen Beispielen, in denen lediglich mit new
ein Objekt erstellt wird.
Während Erzeugungsmuster helfen, das Was und Wie beim Erzeugen von Objekten zu trennen, helfen Strukturmuster, um Klassen und ihre Beziehungen zu modellieren. Sie beschreiben wie der Name schon sagt die Struktur eines objektorientierten Programms.
Ein sehr einfaches Erzeugungsmuster ist der Adapter. Es handelt sich dabei um eine Klasse, die eine andere Klasse kapselt, um eine neue Schnittstelle anzubieten.
#include <string> #include <algorithm> #include <cctype> class adapter_string { public: adapter_string(std::string str) : s(str) { } std::string str() const { return s; } void to_upper() { std::transform(s.begin(), s.end(), s.begin(), std::toupper); } private: std::string s; };
Obige Klasse adapter_string
basiert auf der Klasse std::string
. Sie bietet eine neue Methode an, um eine Zeichenkette in Großbuchstaben umzuwandeln.
Während im obigen Beispiel der Adapter willkürlich definiert ist und eine Funktion zum Umwandeln von Zeichenketten in Großbuchstaben auch ohne die Definition einer neuen Klasse erstellt werden kann, sind Adapter notwendig, wenn zum Beispiel auf eine existierende Schnittstelle zugegriffen werden muss, die Schnittstelle aber mit Zuhilfenahme einer anderen Klasse implementiert werden soll. Wenn die Schnittstelle nicht geändert werden kann, weil sie zum Beispiel fest in eine Klassenhierarchie integriert ist, wird sie als Adapter implementiert: Funktionen der Schnittstelle greifen hierbei auf die entsprechenden Funktionen der Klasse zu, mit deren Hilfe die Schnittstelle implementiert wird.
In gewisser Weise dem Adapter ähnlich ist der Dekorierer. Während Sie den Adapter über eine dritte unabhängige Klasse überstülpen können, um eine neue Schnittstelle zu bieten, wird der Dekorierer von einer Schnittstelle abgeleitet und verweist gleichzeitig auf die Schnittstelle. Aufgrund der Vererbung ist dem Dekorierer eine Schnittstelle vorgegeben, die er zwar erweitern, aber nicht ändern kann.
#include <iostream> class fliegend { public: virtual void fliegen() = 0; }; class vogel : public fliegend { public: void fliegen() { std::cout << "Pieppiep" << std::endl; } }; class dekorierer : public fliegend { public: dekorierer(fliegend &fl) : f(fl) { } void fliegen() { std::cout << "Hier fliegt was: " << std::endl; f.fliegen(); } private: fliegend &f; }; int main() { vogel v; dekorierer d(v); d.fliegen(); }
Anstatt den Vogel fliegen zu lassen, wird in main()
auf einen Dekorierer zugegriffen, der sich - weil er von der gleichen Schnittstelle abgeleitet wurde - wie ein Vogel verhält. Der Dekorierer im obigen Beispiel gibt nun in der Methode fliegen()
eine weitere Meldung auf den Bildschirm aus, bevor er den Methodenaufruf an ein anderes Objekt vom Typ fliegend
weiterleitet: Er dekoriert also den Methodenaufruf.
Da Dekorierer alle von der gleichen Klasse abgeleitet sind und Methodenaufrufe weiterleiten, lassen sie sich hintereinander schalten. Wären mehrere Dekorierer definiert, könnten sie wie folgt eingesetzt werden.
vogel v; dekorierer1 d1(v); dekorierer2 d2(d1); dekorierer3 d3(d2); d3.fliegen();
Das Strukturmuster Dekorierer wird eingesetzt, wenn zwar Klassen aus einer Hierarchie verwendet werden sollen, die Funktionsweise der Klassen aber hier und da angepasst werden muss. Indem Dekorierer definiert und in die Klassenhierarchie integriert werden, können sie quasi vor jede Klasse aus der Hierarchie vorgeschaltet werden.
Der entscheidende Unterschied zwischen dem Adapter und dem Dekorierer ist, dass der Adapter ein Objekt besitzt, während der Dekorierer auf ein Objekt verweist. Erst das ermöglicht nämlich das Hintereinanderschalten von Dekorierern.
Ein etwas komplizierteres Strukturmuster ist das Kompositum. Es wird dann eingesetzt, wenn eine Unterordnung oder Verästelung von Objekten notwendig ist. Um zum Beispiel ein Verzeichnissystem in der Objektorientierung zu modellieren, kann das Kompositum angewandt werden.
#include <vector> struct verzeichnis_eintrag { }; struct verzeichnis : public verzeichnis_eintrag { std::vector<verzeichnis_eintrag*> eintraege; }; struct datei : public verzeichnis_eintrag { };
Im obigen Beispiel sind die beiden Klassen verzeichnis
und datei
von der Klasse verzeichnis_eintrag
abgeleitet. Die Klasse verzeichnis
besitzt eine Eigenschaft vom Typ std::vector<verzeichnis_eintrag*>
, über die auf Verzeichniseinträge verwiesen werden kann. Da sowohl die Klasse verzeichnis
als auch die Klasse datei
von verzeichnis_eintrag
abgeleitet sind, kann ein Verzeichnis also Dateien oder andere Verzeichnisse enthalten, die wiederum Dateien oder andere Verzeichnisse enthalten können.
Das Kompositum wird dann eingesetzt, wenn ein Objekt andere Objekte vom gleichen Typ enthalten muss, die sich aber wie das eigene Objekt verhalten und ihrerseits auf andere Objekte vom gleichen Typ zugreifen können müssen. Dies wird erreicht, indem eine Klasse auf ihre Elternklasse verweist, so wie es die Klasse verzeichnis
oben tut.
Das Kompositum ist insofern dem Dekorierer ähnlich als dass bei beiden Strukturmustern eine Kindklasse auf eine Elternklasse verweist. Die entscheidenden Unterschiede sind, dass beim Kompositum die Kindklassen Objekte besitzen, während der Dekorierer nur eine Referenz oder einen Zeiger auf ein bereits existierendes Objekt erhält. Außerdem kann beim Kompositum eine Kindklasse mehr als ein Objekt besitzen, während der Dekorierer auf genau ein Objekt zugreift.
Ein weiteres Strukturmuster, das in diesem Artikel vorgestellt werden soll, ist das Fliegengewicht. Das Fliegengewicht kommt dann zum Einsatz, wenn eine große Anzahl an Objekten erstellt werden soll, deren Eigenschaften aber großenteils gleiche Werte besitzen. Anstatt nun in jedem Objekt die gleichen Daten zu speichern, werden diese in eine anderes Objekt ausgelagert.
#include <string> struct adresse { std::string name; std::string strasse; std::string ort; std::string *land; };
In obiger Struktur adresse
werden verschiedene Daten gespeichert, die allgemein zu einer Adresse gehören. Da es jedoch eine überschaubare Anzahl von Ländern gibt, werden viele Adressen im selben Land sein. Anstatt nun in jeder Adresse eine Zeichenkette wie Deutschland zu speichern, kann ein Objekt vom Typ std::string
dynamisch mit new
erstellt werden, auf das alle Adressen verweisen und das allen Adressen, die sich in Deutschland befinden, gehört.
Bei Anwendung des Fliegengewichts ist zu beachten, dass in irgendeiner Weise mitgezählt werden muss, von wie vielen anderen Objekten ein Objekt benutzt wird. Würden zum Beispiel alle Adressen im Programm gelöscht werden, müsste die Adresse, die als letzte entfernt wird, das Objekt vom Typ std::string
zerstören, in dem das Land gespeichert wird. Für den Fall, dass das Land einer Adresse neu gesetzt wird, darf außerdem nicht einfach das Objekt vom Typ std::string
neu gesetzt werden, weil dann für alle Adressen, die auf das gleiche Objekt verweisen, das Land geändert werden würde. Für die Anwendung des Strukturmusters Fliegengewicht ist also typischerweise zusätzlicher Code nötig.
Die dritte Kategorie an Design Patterns sind die Verhaltensmuster. Sie helfen Entwicklern, das Verhalten eines Programms zu modellieren. Während Strukturmuster statisch sind und die Struktur eines Programms zu einem Zeitpunkt beschreiben, stellen Verhaltensmuster Änderungen innerhalb eines Programms im Zeitablauf dar.
Ein aus dem C++-Standard bekanntes Verhaltensmuster ist der Iterator. Container-Klassen wie std::vector
bieten Iteratoren an, mit denen über die Elemente in einer Container-Klasse gewandert werden kann. Ein Iterator zeigt also auf ein Element in einem Container und kann bewegt werden, um zum Beispiel auf das nachfolgende Element im Container zu zeigen.
#include <vector> #include <iostreams> int main() { std::vector<int> v; v.push_back(99); v.push_back(100); std::vector<int>::iterator it = v.begin(); std::cout << *it << std::endl; std::cout << *++it << std::endl; }
Im obigen Beispiel wird ein Vektor vom Typ int
mit den beiden Zahlen 99 und 100 gefüllt. Dann wird die Method begin()
aufgerufen, die einen Iterator zurückgibt, der auf die erste Zahl im Vektor zeigt. Über das Sternchen - den *
-Operator - kann auf die Zahl zugegriffen werden, auf die der Iterator momentan zeigt, um sie zum Beispiel auf den Monitor auszugeben. In der letzten Zeile in der Funktion main()
wird der Iterator dann mit ++
um eine Stelle vorgerückt, so dass er auf die zweite Zahl im Vektor zeigt.
Der Vorteil von Iteratoren ist, dass sie Zugriffe auf Stellen in Containern ermöglichen unabhängig davon, welche Art von Container verwendet wird. Die Algorithmen, die im C++-Standard definiert sind, werden zum Beispiel nicht direkt auf Container angewandt, sondern auf Iteratoren. Das macht es möglich, Algorithmen, die zum Beispiel Zahlen sortieren, einmal zu definieren. Sie können trotzdem auf unterschiedliche Container angewandt werden, weil der Zugriff auf Container über Iteratoren möglich ist.
#include <vector> #include <iostreams> #include <algorithm> int main() { std::vector<int> v; v.push_back(99); v.push_back(100); std::deque<int> d; d.push_back(50); d.push_back(51); std::sort(v.begin(), v.end()); std::sort(d.begin(), d.end()); }
Im obigen Beispiel wird die Funktion std::sort()
verwendet, um die Zahlen in den beiden Containern v und d zu sortieren. Obwohl die beiden Container auf unterschiedlichen Datentypen basieren - einmal auf std::vector<int>
, einmal auf deque<int>
- können sie dank Iteratoren mit der gleichen Funktion sortiert werden.
Das Entwurfsmuster des Beobachters kann nicht durch ein Beispiel aus dem C++-Standard erläutert werden, ist aber ähnlich einfach zu verstehen wie Iteratoren. Es kommt in vielen Klassenbibliotheken zur Anwendung und ist daher ein Entwurfsmuster, das man recht häufig antrifft.
#include <vector> #include <algorithm> #include <functional> #include <iostream> struct beobachter { virtual void mausklick() = 0; }; class fenster { public: void registriere(beobachter &b) { beobachter.push_back(&b); } private: void betriebssystem_meldet_mausklick() { std::for_each(beobachter.begin(), beobachter.end(), std::mem_fun(&beobachter::mausklick)); } std::vector<beobachter*> beobachter; }; struct mein_beobachter : public beobachter { void mausklick() { std::cout << "Mausklick" << std::endl; } }; int main() { fenster f; mein_beobachter b; f.registriere(b); }
Im obigen Beispiel wird eine Klasse fenster
definiert, die ein Fenster repräsentieren soll, wie man es von grafischen Betriebssystemen wie Microsoft Windows her kennt. Bei einem Mausklick innerhalb des Fensters sollen nun andere Objekte informiert werden, damit sie dann geeignet auf den Mausklick reagieren können.
Um die Klasse fenster
möglichst flexibel zu gestalten, greift sie auf Objekte zu, die erst registriert werden müssen. Damit unterschiedliche Objekte registriert werden können, erwartet die Klasse fenster
lediglich, dass die Objekte auf einer Klasse basieren, die von beobachter
abgeleitet ist.
Im obigen Beispiel wird nun eine neue Klasse mein_beobachter
definiert, dessen Elternklasse beobachter
ist. Weil beobachter
eine abstrakte Klasse ist, muss mein_beobachter
die Methode mausklick()
implementieren.
Innerhalb der Funktion main()
wird die Klasse fenster
instantiiert. Die Methode registriere()
erhält eine Referenz auf ein Objekt vom Typ mein_beobachter
, obwohl die Klasse fenster
die Klasse mein_beobachter
gar nicht kennt. Da mein_beobachter
jedoch von beobachter
abgeleitet ist, kann das Objekt b registriert werden.
Die Klasse fenster
enthält bespielhaft eine Methode betriebssystem_meldet_mausklick()
, die - so wird für den Moment angenommen - vom Betriebssystem aufgerufen wird, wenn der Anwender mit der Maus ins Fenster klickt. Innerhalb der Methode wird dann auf die registrierten Beobachter zugegriffen und für jeden die Methode mausklick()
aufgerufen. Da alle Beobachter von der Klasse beobachter
abgeleitet sein müssen - sonst könnten sie gar nicht als Parameter bei der Registrierung an die Methode registriere()
übergeben worden sein - besitzen alle Beobachter eine Methode mausklick()
. Auf diese Weise können unterschiedliche Objekte informiert werden und reagieren, wenn ein Anwender mit der Maus ins Fenster klickt.
Ein weiteres Verhaltensdiagramm, das abschließend in diesem Artikel vorgestellt werden soll, ist der Zustand. Er setzt voraus, dass ein Objekt jeweils genau einen Zustand einnimmt und diesen Zustand bei bestimmten Ereignissen wechselt.
#include <iostream> #include <stdexcept> struct fernseher_status { void einschalten() { throw std::runtime_error("Fehler"); } void ausschalten() { throw std::runtime_error("Fehler"); } }; struct fernseher_an : public fernseher_status { void ausschalten() { std::cout << "Fernseher aus" << std::endl; } }; struct fernseher_aus : public fernseher_status { void einschalten() { std::cout << "Fernseher ein" << std::endl; } }; class fernseher { public: fernseher() : status(new fernseher_aus) { } ~fernseher() { delete status; } void einschalten() { status->einschalten(); delete status; status = new fernseher_an; } void ausschalten() { status->ausschalten(); delete status; status = new fernseher_aus; } private: fernseher_status *status; };
Im obigen Beispiel wird ein Fernseher modelliert, der genau zwei Zustände unterstützt: An und aus. Dazu wurde eine Klasse fernseher_status
definiert, die alle Funktionen, die der Fernseher bieten soll, implementiert. Die Implementierungen werfen jedoch alle eine Ausnahme.
Von dieser Klasse abgeleitet sind die beiden Klassen fernseher_an
und fernseher_aus
. Diese Klassen repräsentieren die beiden Zustände, die der Fernseher einnehmen kann. In diesen beiden Klassen sind auch jeweils die Methoden überschrieben, die für den jeweiligen Zustand von Bedeutung sind und eine sinnvolle Funktion haben. Die Klasse fernseher_aus
zum Beispiel überschreibt lediglich die Methode einschalten()
und nicht ausschalten()
, da ein ausgeschalteter Fernseher sich nicht nochmal ausschalten lässt.
Die Klasse fernseher
leitet Methodenaufrufe an ein Objekt vom Typ fernseher_status
weiter. Es hängt nun vom Aufrufer ab, ob er den Fernseher richtig bedient und nicht versucht, ihn zum Beispiel zweimal hintereinander auszuschalten. Falls die Weiterleitung des Methodenaufrufs gelingt und keine Ausnahme geworfen wird, wird der Zustand des Fernsehers entsprechend geändert. So wird in der Methode ausschalten()
zum Beispiel ein Objekt vom Typ fernseher_aus
instantiiert und in der Eigenschaft status gespeichert, da der Fernseher nun aus ist.
Der Vorteil des Entwurfsmusters Zustand ist, dass bei Objekten, die im Gegensatz zum Fernseher oben mehr als zwei Zustände unterstützen, keine vielen if
-Verzweigungen entwickelt werden müssen, um zu überprüfen, in welchem Zustand sich ein Objekt momentan befindet und in welchen Zustand es als nächstes wechseln muss. Stattdessen werden Zustände, die ein Objekt unterstützt, als Klassen definiert. Da nur Aktionen, die ein Objekt im jeweiligen Zustand unterstützt, als gültige Methoden implementiert werden, schafft dies eine Struktur, die besser zur Übersicht beitragen soll als ein Gewirr von if
-Verzweigungen.
Die in diesem Artikel vorgestellten Design Patterns werden in der Praxis in unterschiedlichen Formen eingesetzt. Sie finden eher selten Klassen, die sich ganz genau an die theoretischen Vorgaben halten, die ein bestimmtes Design Pattern ausmachen. So kann es durchaus üblich sein, dass ein Design Pattern nicht vollständig implementiert wurde oder aber mehrere Design Patterns gleichzeitig.
Das Ziel für Sie als Entwickler ist es nicht, bei der objektorientierten Modellierung möglichst viele Design Patterns einzusetzen. Ein C++-Programm, das auf mehr Design Patterns basiert als ein anderes, ist nicht automatisch besser. Design Patterns wollen vielmehr ein Hilfsinstrument sein, das Ihren Horizont erweitert und Ihnen neue Möglichkeiten bei der Modellierung aufzeigt, auf die Sie vielleicht nicht ohne weiteres gekommen wären.
Für erfahrene objektorientierte Entwickler spielen Design Patterns bei der Modellierung eine eher untergeordnete Rolle. Sie werden intuitiv angewandt, weil Design Patterns für erfahrene Entwickler je nach Situation das verständlichste Modell darstellen. In anderen Situationen, in denen Design Patterns weniger verständlich sind, weil sie nicht wie die Faust aufs Auge zum Modell passen, werden sie nicht eingesetzt. Design Patterns müssen zum Modell passen und nicht das Modell zu Design Patterns.
In vielen C++-Programmen greifen Entwickler standardmäßig auf Bibliotheken zu. So ist die Suche nach passenden Bibliotheken für Entwickler eine nicht ungewöhnliche Aufgabe. Eine geeignete Bibliothek zu finden setzt voraus, dass ein Entwickler sich des Problems bewusst ist, das er mit einer Bibliothek zu lösen gedenkt.
Die Boost C++-Bibliotheken sind eine große Sammlung allgemein nützlicher Bibliotheken, die sowohl in privaten als auch in kommerziellen Projekten kostenlos eingesetzt werden können. Zu dieser Sammlung zählt unter anderem eine Bibliothek, die Flyweight heißt. Diese Bibliothek macht es möglich, das gleichnamige Entwurfsmuster - nämlich Fliegengewicht - sehr einfach einzusetzen. Dabei ist C++-Entwicklern zum Teil gar nicht bewusst, dass diese Bibliothek auf einem Design Pattern basiert und dass sie mit dieser Bibliothek ein Design Pattern einsetzen. Der Nutzen der Bibliothek für das eigene Programm steht im Vordergrund - ob die Bibliothek auf einem Design Pattern basiert und wie dessen Name lautet spielt keine Rolle.
Das Studieren von Design Patterns ist empfehlenswert, um sich die Denkweise von erfahrenen Entwicklern anzueignen. Design Patterns sollen helfen zu verstehen, wie erfahrene Entwickler ihre Modelle erstellen. Wenn Sie eines Tages dann selbst erfahrener Entwickler sind, werden Sie Design Patterns automatisch in der Modellierung einsetzen und brauchen sich ihrer gar nicht mehr bewusst sein.
Copyright © 2008, 2009 Boris Schäling