[X] Das State-Pattern - Des Kaisers neue Kleider
-
Das State-Pattern - des Kaisers neue Kleider
Objektorientierte Programmierung Für Dummies | ISBN: 9783527700578Der folgende Text war ursprünglich für mein Buch „Objektorientierte Programmierung für Dummies“ geschrieben worden, und hätte im Buch ab Seite 424 erscheinen sollen. Aus Platzgründen wurde er aber nicht mehr berücksichtigt.
Inhalt
In diesem Artikel erfahren Sie, wie man mit Hilfe des State-Pattern das Innenleben von Objekten ändert, ohne dass man es von außen sieht und als Benutzer oder Besitzer eines Objekts bemerkt. Dadurch wird die Kopplung eines übergeordneten Objekts zu dem benutzten Objekt reduziert.
- 1 Motivation – des Kaisers neue Kleider
- 2 Das State-Pattern
- 3. Zusammenfassung
- 4 Das Pimpl-Idiom
1 Motivation – des Kaisers neue Kleider
Menschen ändern sich, Objekte auch. Nehmen Sie eine Spielfigur, die in zwei Modi unterwegs sein kann: laufend und schwimmend. Oder kämpfend und bauend. Wer es bodenständiger mag, nimmt einen Server, der an einem Netzwerk hängt. Mal wartet der Server auf Verbindungen, mal besteht eine aktive Verbindung, mal geht der Server in einen Fehlerzustand.
Alle diese Dinge haben eines gemeinsam, in Abhängigkeit seines Zustands ändert ein Objekt sein Verhalten. Nehmen Sie an, dass sich zwei Zustände durch zwei Klassen beschreiben lassen, so könnte man das ja ungefähr so aussehen lassen:class State1 : public BasisState; class State2 : public BasisState; BasisState* pState = new State1(); pState->doSomething(); delete pState; pState = new State2(); pState->doSomething();
Über eine virtuelle Methode kann das Objekt
pState
sich dann je nach Zustand richtig verhalten. Eine gute Lösung? Leider nein. Bedenken Sie, dass andere Objekte bereits Zeiger und Referenzen auf dieses Objekt besitzen können. Und nun wollen Sie alle bestehenden Zeiger und Referenzen durch neue ersetzen?
Das geht nicht. Das Objekt selbst darf sich für einen Außenstehenden nicht verändern.2 Das State-Pattern
Ein Entwurfsmuster, das dieses Problem löst, nennt sich State-Pattern. Um ein Objekt seinen inneren Zustand ändern zu lassen, benötigt man insgesamt vier Klassen (für zwei Zustände). Wollte man drei Zustände modellieren, bräuchte man fünf Klassen (für n Zustände immer 2 + n Klassen).
Die Abbildung zeigt den Zusammenhang zwischen den vier Klassen bei der Realisierung des State-Pattern.
Von außen wird nur die Klasse
Dummy
sichtbar. Intern gibt es für jeden Zustand eine eigene Klasse, diese sind von einer gemeinsamen Basisklasse abgeleitet. Jede Methode vonDummy
hat eine entsprechende virtuelle Methode inInnerDummy
.
Betrachten Sie ein Programmbeispiel zunächst aus Sicht des Aufrufers.#include <iostream> #include "dummy.h" using namespace std; int main() { Dummy dummy(5); for (int i = 0; i < 1000; i++) { dummy.doSomething(); dummy.showValue(); } return 0; }
Wie Sie sehen können, erfährt man als Nutzer der Klasse
Dummy
nichts von deren Eigen- und Innenleben. Das ist schon mal ein guter Beginn. Nun geht es weiter zu den inneren Klassen.#ifndef _DUMMY_H #define _DUMMY_H class Dummy; class InnerDummy { public: virtual void showValue(Dummy* pDummy) = 0; }; class InnerDummyA : public InnerDummy { public: virtual void showValue(Dummy* pDummy); }; class InnerDummyB : public InnerDummy { public: virtual void showValue(Dummy* pDummy); }; class Dummy { public: Dummy(int value); ~Dummy(); void showValue() { _pDummy->showValue(this); } void doSomething(); int getValue() const { return _value; } private: class InnerDummy* _pDummy; int _value; }; #endif
Beachten Sie, dass es für die Methode
Dummy::showValue
auch noch virtuelle Methoden inInnerDummy
(und natürlich auch den abgeleiteten Klassen) gibt. Zudem besitztDummy
einen Zeiger aufInnerDummy
– wozu sehen Sie, sobald Sie sich die mplementation betrachten.#include <iostream> #include "dummy.h" using namespace std; void InnerDummyA::showValue(Dummy* pDummy) { cout << "Jetzt bin ich A! " << pDummy->getValue() <<endl; } void InnerDummyB::showValue(Dummy* pDummy) { cout << "Jetzt bin ich B! " << pDummy->getValue() << endl; } Dummy::Dummy(int value) : _value(value) { _pDummy = new InnerDummyA(); } Dummy::~Dummy() { delete _pDummy; } void Dummy::doSomething() { delete _pDummy; if (rand() % 2) { _pDummy = new InnerDummyA(); } else { _pDummy = new InnerDummyB(); } }
Jede der
show
-Methoden wird inInnerDummyA
undInnerDummyB
anders implementiert, das ist auch logisch, schließlich ändert sich mit dem Zustand das Verhalten. Im Konstruktor vonDummy
wird zunächst einInnerDummyA
-Objekt erzeugt, das Objekt befindet sich zu Beginn im Zustand »A«. Ruft man von außenDummy::showValue
auf, so wird in Wirklichkeit die virtuelle Methode des zurzeit eingehängtenInnerDummy
-Objekts aufgerufen. Je nach Zustand des ObjektsDummy
also eine andere Methode.
In der FunktiondoSomething
wird ein Zustandswechsel simuliert, hier einfach durch die Auswertung einer Zufallszahl. Je nach Ergebnis wechselt das Dummy-Objekt mal in den Zustand »A«, mal in den Zustand »B«. Sie sehen an der Bildschirmausgabe, dass der Aufrufer immershowValue
aufruft, die Ausgabe aber dennoch unterschiedlich sein kann.3. Zusammenfassung
Das State-Pattern ermöglicht es einem Objekt je nach innerem Zustand ein anderes Verhalten zu zeigen.
Implementiert wird das State-Pattern dadurch, dass man für jeden Zustand eine eigene Klasse realisiert. Diese besitzen eine gemeinsame Basisklasse. Für jede Funktion, deren Verhalten sich ändern soll, wird in der Basisklasse eine virtuelle Methode angelegt, die in den abgeleiteten Klassen überschrieben wird. Ruft man von außen eine Methode des Objekts auf, wird in Wirklichkeit eine Methode des gerade aktiven inneren Zustand-Objekts aufgerufen.
Interessant ist das State-Pattern auch, falls jeder Zustand des Objekts noch eigene Variablen benötigt. Packt man alles in eine einzige Klasse, so landen letztlich viele Variablen in einer einzigen Klasse, die aber je nach Zustand immer nur einen Teil benötigt. Bei der State-Lösung ist jede Zustand-Klasse so schlank wie möglich.
4 Das Pimpl-Idiom
Rein technisch sieht das State-Pattern einem anderen Trick sehr ähnlich, dem so genannten Pimpl-Idiom. Dort besitzt eine äußere Klasse nur einen Zeiger auf eine innere Klasse und sonst nichts, ganz ähnlich zum State-Pattern:
class Inner; class Outer { public: Outer() { _pImpl = new Inner(); } void doSomething() { _pImpl->doSomething(); } private: Inner* _pImpl; };
Auch hier ist es so, dass die Klasse
Inner
genau die gleichen Methoden besitzt wie die äußere KlasseOuter
, aber nicht, um Zustände zu modellieren. Der Gag an Pimpl ist, dass man die komplette KlasseInner
in einer anderen Datei implementieren kann. Hat Sie nicht auch schon oft gestört, dass bei der Ergänzung einer privaten Variablen in einer Klasse plötzlich alles neu kompiliert werden muss? Bei Pimpl wird dies vermieden, da sämtliche privaten Variablen und Methoden nur in Inner realisiert werden. Die KlasseOuter
ändert sich dadurch nicht und man kannInner
verändern, ohne das komplette Projekt neu kompilieren zu müssen, es reicht aus,Inner
neu zu übersetzen. Pimpl nennt man deshalb auch Compiler-Firewall.Der Begriff Pimpl kommt von pointer to implementation und erinnert daran, dass ein Zeiger auf das tatsächliche Implementations-Objekt benutzt wird, um die Klasse zu realisieren. Das Pimpl-Idiom wird oft in größeren Open-Source-Projekten verwendet, in denen viele Leute viele verschiedene Dateien bearbeiten.
-
In meinen Augen abgenickt
Zwei keine Anmerkungen hab ich allerdings:
- Über führende Unterstriche bei Variablennamen kann man immer herzlichst diskutieren. Um solche Diskussionen garnicht erst aufkommen zu lassen kann man es denke ich vertreten, einen andere Namenskonvention für private Variablen zu wählen
- Du erwänhnst das Pimpl-Idiom als engen Verwandten. Da es hier im Forum des öfteren erwähnt wird wäre es imho sinnvoll, den Namen mit in den Titel aufzunehmen.
-
Mach' mal einen konkreten Vorschlag zu der Benennung, wie Du nämlich siehst, verwende ich bei den Initialisierungen für den Parameter der Funktion und den Member immer den gleichen Namen. Nach der Logik müßte ich ja dann überall "this->" schreiben, aber dann sieht das aus wie PHP.
-
Zur Begründung für meine Bedenken:
17.4.3.1.2 Global names [lib.global.names] 1 Certain sets of names and function signatures are always reserved to the implementation: Each name that contains a double underscore (_ _) or begins with an underscore followed by an upper case letter (2.11) is reserved to the implementation for any use. Each name that begins with an underscore is reserved to the implementation for use as a name in the global namespace. Such names are also reserved in namespace ::std
Ok, here we go
#ifndef DUMMY_H //kann beliebig verlaengert werden, z.B. durch namespace-namen #define DUMMY_H class Dummy { private: class InnerDummy* pInnerDummy; int value; public: }; //... Dummy::Dummy(int value) : value(value) //tut genau das was es soll! { pInnerDummy = new InnerDummyA(); } //... class Outer { public: Outer() { pImpl = new Inner(); } void doSomething() { pImpl->doSomething(); } private: Inner* pImpl; };
Alternativ kann man auch mit angehängten Unterstrichen arbeiten, ist Geschmackssache aber meist nicht nötig)
Nach der Logik müßte ich ja dann überall "this->" schreiben, aber dann sieht das aus wie PHP.
Nur wenn du in den Argumentlisten von Methoden die gleichen Namen verwendest. Wie gezeigt brauchst du es nicht wenn Konstruktorargumente in Initialisierungslisten verwendet werden.
-
Sehe da zwei Fehler noch:
1. Dein Code enthält Memoryleaks
2. class InnerDummy* dummy; -> InnerDummy* dummy;
-
Das State-Pattern - des Kaisers neue Kleider
Objektorientierte Programmierung Für Dummies | ISBN: 9783527700578Der folgende Text war ursprünglich für mein Buch „Objektorientierte Programmierung für Dummies“ geschrieben worden, und hätte im Buch ab Seite 424 erscheinen sollen. Aus Platzgründen wurde er aber nicht mehr berücksichtigt.
Inhalt
In diesem Artikel erfahren Sie, wie man mit Hilfe des State-Pattern das Innenleben von Objekten ändert, ohne dass man es von außen sieht und als Benutzer oder Besitzer eines Objekts bemerkt. Dadurch wird die Kopplung eines übergeordneten Objekts zu dem benutzten Objekt reduziert.
- 1 Motivation – des Kaisers neue Kleider
- 2 Das State-Pattern
- 3 Zusammenfassung
- 4 Das Pimpl-Idiom
1 Motivation – des Kaisers neue Kleider
Menschen ändern sich, Objekte auch. Nehmen Sie eine Spielfigur, die in zwei Modi unterwegs sein kann: laufend und schwimmend. Oder kämpfend und bauend. Wer es bodenständiger mag, nimmt einen Server, der an einem Netzwerk hängt. Mal wartet der Server auf Verbindungen, mal besteht eine aktive Verbindung, mal geht der Server in einen Fehlerzustand.
Alle diese Dinge haben eines gemeinsam, in Abhängigkeit seines Zustands ändert ein Objekt sein Verhalten. Nehmen Sie an, dass sich zwei Zustände durch zwei Klassen beschreiben lassen, so könnte man das ja ungefähr so aussehen lassen:class State1 : public BasisState; class State2 : public BasisState; BasisState* pState = new State1(); pState->doSomething(); delete pState; pState = new State2(); pState->doSomething();
Über eine virtuelle Methode kann das Objekt
pState
sich dann je nach Zustand richtig verhalten. Eine gute Lösung? Leider nein. Bedenken Sie, dass andere Objekte bereits Zeiger und Referenzen auf dieses Objekt besitzen können. Und nun wollen Sie alle bestehenden Zeiger und Referenzen durch neue ersetzen?
Das geht nicht. Das Objekt selbst darf sich für einen Außenstehenden nicht verändern.2 Das State-Pattern
Ein Entwurfsmuster, das dieses Problem löst, nennt sich State-Pattern. Um ein Objekt seinen inneren Zustand ändern zu lassen, benötigt man insgesamt vier Klassen (für zwei Zustände). Wollte man drei Zustände modellieren, bräuchte man fünf Klassen (für n Zustände immer 2 + n Klassen).
Die Abbildung zeigt den Zusammenhang zwischen den vier Klassen bei der Realisierung des State-Pattern.
Von außen wird nur die Klasse
Dummy
sichtbar. Intern gibt es für jeden Zustand eine eigene Klasse, diese sind von einer gemeinsamen Basisklasse abgeleitet. Jede Methode vonDummy
hat eine entsprechende virtuelle Methode inInnerDummy
.
Betrachten Sie ein Programmbeispiel zunächst aus Sicht des Aufrufers.#include <iostream> #include "dummy.h" using namespace std; int main() { Dummy dummy(5); for (int i = 0; i < 1000; i++) { dummy.doSomething(); dummy.showValue(); } return 0; }
Wie Sie sehen können, erfährt man als Nutzer der Klasse
Dummy
nichts von deren Eigen- und Innenleben. Das ist schon mal ein guter Beginn. Nun geht es weiter zu den inneren Klassen.#ifndef DUMMY_H #define DUMMY_H class Dummy; class InnerDummy { public: virtual void showValue(Dummy* pDummy) = 0; }; class InnerDummyA : public InnerDummy { public: virtual void showValue(Dummy* pDummy); }; class InnerDummyB : public InnerDummy { public: virtual void showValue(Dummy* pDummy); }; class Dummy { public: Dummy(int value); ~Dummy(); void showValue() { pDummy->showValue(this); } void doSomething(); int getValue() const { return value; } private: InnerDummy* pDummy; int value; }; #endif
Beachten Sie, dass es für die Methode
Dummy::showValue
auch noch virtuelle Methoden inInnerDummy
(und natürlich auch den abgeleiteten Klassen) gibt. Zudem besitztDummy
einen Zeiger aufInnerDummy
– wozu sehen Sie, sobald Sie sich die mplementation betrachten.#include <iostream> #include "dummy.h" using namespace std; void InnerDummyA::showValue(Dummy* pDummy) { cout << "Jetzt bin ich A! " << pDummy->getValue() <<endl; } void InnerDummyB::showValue(Dummy* pDummy) { cout << "Jetzt bin ich B! " << pDummy->getValue() << endl; } Dummy::Dummy(int value) : value(value) { pDummy = new InnerDummyA(); } Dummy::~Dummy() { delete pDummy; } void Dummy::doSomething() { delete pDummy; if (rand() % 2) { pDummy = new InnerDummyA(); } else { pDummy = new InnerDummyB(); } }
Jede der
show
-Methoden wird inInnerDummyA
undInnerDummyB
anders implementiert, das ist auch logisch, schließlich ändert sich mit dem Zustand das Verhalten. Im Konstruktor vonDummy
wird zunächst einInnerDummyA
-Objekt erzeugt, das Objekt befindet sich zu Beginn im Zustand »A«. Ruft man von außenDummy::showValue
auf, so wird in Wirklichkeit die virtuelle Methode des zurzeit eingehängtenInnerDummy
-Objekts aufgerufen. Je nach Zustand des ObjektsDummy
also eine andere Methode.
In der FunktiondoSomething
wird ein Zustandswechsel simuliert, hier einfach durch die Auswertung einer Zufallszahl. Je nach Ergebnis wechselt das Dummy-Objekt mal in den Zustand »A«, mal in den Zustand »B«. Sie sehen an der Bildschirmausgabe, dass der Aufrufer immershowValue
aufruft, die Ausgabe aber dennoch unterschiedlich sein kann.3 Zusammenfassung
Das State-Pattern ermöglicht es einem Objekt je nach innerem Zustand ein anderes Verhalten zu zeigen.
Implementiert wird das State-Pattern dadurch, dass man für jeden Zustand eine eigene Klasse realisiert. Diese besitzen eine gemeinsame Basisklasse. Für jede Funktion, deren Verhalten sich ändern soll, wird in der Basisklasse eine virtuelle Methode angelegt, die in den abgeleiteten Klassen überschrieben wird. Ruft man von außen eine Methode des Objekts auf, wird in Wirklichkeit eine Methode des gerade aktiven inneren Zustand-Objekts aufgerufen.
Interessant ist das State-Pattern auch, falls jeder Zustand des Objekts noch eigene Variablen benötigt. Packt man alles in eine einzige Klasse, so landen letztlich viele Variablen in einer einzigen Klasse, die aber je nach Zustand immer nur einen Teil benötigt. Bei der State-Lösung ist jede Zustand-Klasse so schlank wie möglich.
4 Das Pimpl-Idiom
Rein technisch sieht das State-Pattern einem anderen Trick sehr ähnlich, dem so genannten Pimpl-Idiom. Dort besitzt eine äußere Klasse nur einen Zeiger auf eine innere Klasse und sonst nichts, ganz ähnlich zum State-Pattern:
class Inner; class Outer { public: Outer() { pImpl = new Inner(); } ~Outer() { delete pImpl; } void doSomething() { pImpl->doSomething(); } private: Inner* pImpl; };
Auch hier ist es so, dass die Klasse
Inner
genau die gleichen Methoden besitzt wie die äußere KlasseOuter
, aber nicht, um Zustände zu modellieren. Der Gag an Pimpl ist, dass man die komplette KlasseInner
in einer anderen Datei implementieren kann. Hat Sie nicht auch schon oft gestört, dass bei der Ergänzung einer privaten Variablen in einer Klasse plötzlich alles neu kompiliert werden muss? Bei Pimpl wird dies vermieden, da sämtliche privaten Variablen und Methoden nur in Inner realisiert werden. Die KlasseOuter
ändert sich dadurch nicht und man kannInner
verändern, ohne das komplette Projekt neu kompilieren zu müssen, es reicht aus,Inner
neu zu übersetzen. Pimpl nennt man deshalb auch Compiler-Firewall.Der Begriff Pimpl kommt von pointer to implementation und erinnert daran, dass ein Zeiger auf das tatsächliche Implementations-Objekt benutzt wird, um die Klasse zu realisieren. Das Pimpl-Idiom wird oft in größeren Open-Source-Projekten verwendet, in denen viele Leute viele verschiedene Dateien bearbeiten.
-
phlox81 schrieb:
Sehe da zwei Fehler noch:
1. Dein Code enthält MemoryleaksBezog sich das auf den weggelassenen D'tor in pImpl?
-
Marc++us schrieb:
phlox81 schrieb:
Sehe da zwei Fehler noch:
1. Dein Code enthält MemoryleaksBezog sich das auf den weggelassenen D'tor in pImpl?
Im ersten Listing fehlt auch noch ein delete.
-
Es sieht recht komisch aus, dass Kapitel 4 nach der Zusammenfassung steht... hast du das aus einem bestimmten Grund gemacht?
-
void Dummy::doSomething() { delete pDummy; if (rand() % 2) { pDummy = new InnerDummyA(); } else { pDummy = new InnerDummyB(); } }
Wenn in einem der Konstruktoren eine Ausnahme geworfen wird, dann gibt es ein Doppeldelete wenn sie nicht vor dem Aufruf von ~Dummy gefangen wird.
-
Oha, sowas würde ich in einem Beispiel normalerweise nicht reinnehmen... auch das erste delete gehört dazu.
Denn irgendwann überlagert die Exaktheit den hopsing point, da es keine Informationen zum eigentlichen Thema hinzufügt.
-
Ich würde es auch nicht extra erwähnen, aber es ist ja nicht schwer das ausnahmesicher zu machen.
void Dummy::doSomething() { InnerDummy*new_dummy; if (rand() % 2) { new_dummy = new InnerDummyA(); } else { new_dummy = new InnerDummyB(); } delete pDummy; pDummy = new_dummy; }
Das Problem ist halt, dass jeder der deinen Code übernimmt und anpasst schon einen schwer zu finden Bug mit übernimmt.
-
Dieser Thread wurde von Moderator/in GPC aus dem Forum Die Redaktion in das Forum Archiv verschoben.
Im Zweifelsfall bitte auch folgende Hinweise beachten:
C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?Dieses Posting wurde automatisch erzeugt.