1. Einleitung
2. Typschaltung mit Funktionspointer-Tabellen in C
3. Implementierung des Objekt-Switching Patterns
4. Verallgemeinerung des Objekt-Switching Patterns
4.1 OnError-Policy
4.2 Singleton-Muster
4.3 Dummy Typ-Parameter
4.4 Implementierung des allgemeinen Objekt-Switching Patterns
5. Typschaltung globaler Funktionen
6. Typschaltung polymorpher Elementfunktionen
7. Erzeugung polymorpher Objekte
8. Schlusswort
9. Referenzen
1. Einleitung
Manchmal ist ein C- oder C++-Programmierer in der bedauernswerten Situation, sich mit ellenlangen switch-Anweisungen der Form
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
//... switch(condition)
{
case condition_1:
//.... break;
case condition_2:
// break;
//... case condition_n:
//... break;
default:
//...
}
//...
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
//... switch(condition)
{
case condition_1:
//.... break;
case condition_2:
// break;
//... case condition_n:
//... break;
default:
//...
}
//...
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
//... switch(condition)
{
case condition_1:
//.... break;
case condition_2:
// break;
//... case condition_n:
//... break;
default:
//...
}
//...
herumplagen zu müssen. Das wäre zunächst mal kein Problem, solange man selber nicht betroffen ist. Leider musste ich schon mehr als ein Review über derartig gestaltete Switch-Gräber ertragen. Das Einzige, was in solchen Fällen hilft, ist Ausdauer und eine volle Kanne mit schwarzem Kaffee der Marke Xtra-Strong. Ganz Hartgesottene schrecken auch nicht davor zurück, verschachtelte Switch-Anweisungen zu produzieren, die sich über hunderte von Zeilen erstrecken. Leider können solche Implementierungen weder vernünftig gewartet noch getestet werden. Wer zu derartigen Konstrukten neigt, sollte sich den folgenden Artikel unbedingt durchlesen.
Dieser Artikel befasst sich mit einem Thema, das ich als Object-Switching bezeichnet habe. Object-Switching behandeln schlicht und ergreifend das immer wiederkehrende Problem, anhand eines Typ-Identifiziers eine Entscheidung treffen zu müssen. Object-Switching ist an das Design-Pattern der Object-Fabriken angelehnt, welches die Erzeugung polymorpher Objekte behandelt.
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
1 2 3 4 5 6 7 8 9 10 11 12 13 14
//...
Base * pObject;
switch(condition)
{
case condition_1:
pObject = new Derived;
break;
case condition_2:
pObject = new AnotherDerived;
break;
//...
}
//...
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
//...
Base * pObject;
switch(condition)
{
case condition_1:
pObject = new Derived;
break;
case condition_2:
pObject = new AnotherDerived;
break;
//...
}
//...
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
//...
Base * pObject;
switch(condition)
{
case condition_1:
pObject = new Derived;
break;
case condition_2:
pObject = new AnotherDerived;
break;
//...
}
//...
Bem.: Dies ist in der Tat eine einfache Objekt-Fabrik
Das nachfolgend vorgestellte Design-Pattern ist für polymorphe und nicht polymorphe Implementierungen gleichermaßen geeignet. Wie wir am Ende des Artikels sehen werden, kann das Muster auch als Objekt-Fabrik verwendet werden. Im Folgendem möchte ich mit euch ein Objekt-Switching-Pattern entwickeln, welches das zu Anfang dargestellte Problem löst.
Es wird vorausgesetzt, dass alle aufrufbaren Entitäten (wie z. Bsp. Funktoren, Funktionsobjekte, etc.) über eine einheitliche Schnittstelle verfügen. Die Schnittstelle ist einheitlich, wenn der Typ des Return-Values und der Parameterliste für alle aufrufbaren Entitäten identisch ist.
Selbstverständlich haben Switch-Anweisungen ihre Berechtigung, sonst wären sie kein Sprachelement von C/C++. Allerdings sollte man nur darauf zurückgreifen, wenn die Anzahl der zu treffenden Entscheidungen nicht überhand nimmt. Mir fallen zu Switch-Case-Anweisungen im Wesentlichen die folgenden Eigenschaften ein:
Abhängig von der Bedingung wird ein Zweig des Codes durchlaufen.
Es gibt keine Vorgaben, wie die Bedingungen in den jeweiligen Zweigen ausprogrammiert werden.
Alle Funktionen müssen innerhalb der Switch-Anweisung bekannt sein.
Es ist mit hohem Aufwand verbunden, den Ablauf des Programms zu ändern.
Hohe Performance, da Switch-Anweisungen direkt durchlaufen werden.
Für den Typ-Identifizierer muss ein integraler Datentyp gewählt werden.
2. Typschaltung mit Funktionspointer-Tabellen in C
In C werden solche statischen Konstrukte häufig durch Funktionspointer-Tabellen ersetzt. Der Code wird durch diese Maßnahme applizierbar, da für eine Änderung lediglich eine Funktion in der Tabelle geändert, eingetragen oder entfernt werden muss. Eine einfache Implementierung könnte z.B. wie folgt aussehen:
Bem.: Die Tabelle array_condition[] simuliert Ereignisse, die z.B. zur Laufzeit angestoßen werden.
Implementierungen, die Funktionspointer-Tabellen verwenden, sind wesentlich übersichtlicher, da Ablauf und Logik an zentraler Stelle in einer Tabelle beschrieben werden. Ich habe die Erfahrung gemacht, dass solche Tabellen auch von Nicht-Programmierern (z. B. Verfahrenstechnikern) gepflegt werden können.
Die Eigenschaften von Implementierungen, die Funktionspointer-Tabellen verwenden, lassen sich wie folgt zusammenfassen:
Es wird ein Typ-Identifizierer benötigt. Im Idealfall kann der Index der Tabelle als Typ-Identifizierer gewählt werden (z.B. für eine State-Maschine).
Zu jeder Bedingung muss eine Funktion geschrieben (Forced by Design).
Alle Funktionen müssen der Tabelle bekannt sein (Compiler-Abhängigkeiten).
Abläufe können einfach geändert werden (z.B. Abändern der Einträge von fptr_tab array_fptr[] ).
Niedrigere Performance, da Typ-Identifizierer gesucht werden müssen (Optimierung z.B. durch binäres Suchen möglich). Diese Eigenschaft trifft natürlich nicht zu, wenn der Tabellenindex als Typ-Identifizierer verwendet wird. Außerdem muss ein zusätzlicher Funktionspointer dereferenziert werden. Der Performanceverlust durch die Dereferenzierung eines Funktionspointer kann normalerweise vernachlässigt werden.
Es kann ein beliebiger Typ-Identifizierer gewählt werden.
3. Implementierung des Objekt-Switching Patterns
Betrachen wir nun aber eine echte C++-Lösung, die zur Laufzeit den Typidentifizierer und die aufrufbare Entität speichert. Zur Speicherung der Daten bietet sich die Zuordnungsliste std::map aus der Standardbibliothek an. Der Container std::map gehört zu den assoziativen Containern, der seine Elemente nach einem frei wählbaren Schlüsselwert sortiert hält. Assoziative Container sind intern als binärer Baum organisiert, was ein schnelles Auffinden der Elemente anhand des Schlüssels ermöglicht. Die Datenelemente bestehen aus Schlüssel-/Datenpaaren vom Typ std::pair. Um Elemente in einen assoziativen Container einzufügen, muss ein Objekt vom Typ std::pair erzeugt werden.
Bem.:value_type ist eine Typedefinition des Containers std::map, mit dessen Hilfe der Paartyp erzeugt wird. Alternativ könnte auch std::make_pair verwendet werden.
Bem.: Die Member-Funktion pair<iterator,bool> insert(const value_type& value) gibt ein Werte-Paar zurück, das einen Iterator auf das soeben eingefügte Element (first) und einen boolschen Wert (second) enthält. Der boolsche Wert gibt Auskunft darüber, ob die Einfügeoperation erfolgreich war.
Um Elemente zu löschen, verwenden wir die Member-Funktion erase.
Bem.: Im public-Bereich der Klasse werden einige Typdefinitionen getätigt, um lästige Schreibarbeiten zu reduzieren. Außerdem finde ich, dass Ausdrücke wie std::map<key_type,data_type>::value_type den Source-Code ziemlich unleserlich machen. Des Weiteren benötigen wir natürlich Methoden zum Hinzufügen, Löschen und Finden von Einträgen.
Schreiben wir gleich ein kleines Programm, um unsere Template-Klasse zu testen.
4. Verallgemeinerung des Objekt-Switching Patterns
4.1 OnError-Policy
Die Methode find wirft eine Exception vom Typ std::runtime. Besser ist es natürlich, wenn der Benutzer unserer Template-Klasse festlegen kann, wie die Klasse im Fehlerfall reagieren soll. Um dies zu erreichen, müssen wir lediglich einen weiteren Template-Parameter hinzufügen. Wird keiner angegeben, soll sich die Klasse wie bisher verhalten.
Für den Default-Fall stellen wir eine Implementierung mit einer statischen Methode zur Verfügung.
Bem.: Die Parameter-Liste einer Template-Klasse darf Default-Typen enthalten. Sie werden verwendet, falls kein Typ angegeben wird. Die Default-Typen dürfen nur am Ende der Parameter-Typ-Liste erscheinen.
Der Benutzer der Template-Klasse hat nun die Möglichkeit, das Verhalten der Methode find von außen zu steuern. Solche Klassen werden in der Literatur als Policy-Klassen bezeichnet. Sie geben eine syntaktische Schnittstelle vor, die strikt eingehalten werden muss.
Wenn die Methode process_error() keine Exception wirft, fehlt das Kriterium, ob der zurückgegebene Datensatz gültig ist. Deshalb ist die Verwendung der Klasse IgnoreError keine gute Idee, da nun das Gültigkeitskriterium auf andere Weise nachgeliefert werden muss. Wird der Default-Parameter OnError vom Aufrufer belegt, sollte er eine Error-Policy implementieren, die eine spezifische Exception wirft.
4.2 Singleton-Muster
Unsere Klasse soll so verändert werden, dass nur eine einzige Instanz erzeugt werden kann. Außerdem soll ein globaler Einstiegspunkt zur Verfügung stehen.
Klassen, die dieser Bedingung genügen, werden als Singleton-Klassen bezeichnet.
Die einfachste Art, das Singleton-Muster zu implementieren, ist die Verwendung einer lokalen statischen Variablen. Dieses Design-Muster wurde erstmalig von Scott Meyers vorgeschlagen.
Beim ersten Aufruf der Funktion wird das Objekt erzeugt und initialisiert. Wenn wir die Singleton-Funktion als statische Methode unserer Klasse implementieren, können wir den Konstruktor privatisieren. Auf diese Weise ist sichergestellt, dass nur eine Objektinstanz erzeugt werden kann. Der Anwender unserer Klasse kann somit keine Instanz der Klasse mehr erzeugen. Stattdessen verwendet er die Elementfunktion instance, um eine Referenz auf das Objekt zu erhalten.
Damit eröffnet sich die Möglichkeit, Funktionspointer dezentral zu registrieren. Die Deklaration der zu registrierenden Funktionen musste bisher der Registrierungstabelle bekannt sein. Nun können wir einen anderen Weg einschlagen. Jedes Modul inkludiert die Registrierungstabelle und trägt die zu registrierende Funktion ein. Damit ist die Verantwortlichkeit an eine andere Stelle verschoben worden. Für die Initialisierung muss lediglich eine statische Dummy-Variable angelegt werden. Da statische Variablen vor dem Aufruf der main-Funktion initialisert werden, sind beim Programmstart alle Einträge in der Registrierungstabelle vorhanden. Ich hoffe, dass jetzt auch klar wird, warum die Methode insert einen boolschen Wert zurückliefert.
Bem.: Es gibt keine Festlegung in welcher Reihenfolge statische Variablen in den jeweiligen Modulen initialisiert werden.
Diese Herangehensweise ist sehr wartungsfreundlich, da nicht mehr in vorhandenen Source-Code eingegriffen werden muss. Stattdessen können wir durch Hinzufügen von Modulen Funktionalitäten erweitern.
4.3 Dummy Typ-Parameter
Da wir das Singleton-Muster implementieren, können wir pro Typ genau eine Instanz erzeugen. Das beschränkt leider die Einsatzmöglichkeiten unserer Template-Klasse, da in manchen Programmen mehrere unabhängige Register-Tabellen desselben Typs benötigt werden. Wir lösen das Problem, indem wir unserer Template-Klasse einen zusätzlichen Default-Dummy-Pararmeter spendieren. Dadurch können wir beliebig viele Typen generieren, ohne die interne Datenstruktur der Registrierungs-Tabelle zu ändern.
4.4 Implementierung des allgemeinen Objekt-Switching Patterns
Nehmen wir nun alle Zutaten und erweitern unsere Template-Klasse um
die Error-Policy
das Singleton-Muster
und den Dummy-Typ-Parameter
Natürlich müssen wir auch eine Implementierung der Default-Error-Policy zur Verfügung stellen. Die Template-Klasse könnte auf folgende Weise realisiert werden.
Im ersten Anwendungbeispiel für unsere Template-Klasse werden globale Funktionen registriert und abhängig von einer Bedingung aufgerufen. Wir betrachten sowohl die Registrierung durch statische Initialisierung als auch die Registrierung innerhalb der main-Funktion. Außerdem nutzen wir die Möglichkeit, mehrere Instanzen der Registrierungstabelle zu erzeugen.
typedef register_table<int, fptr_type,2> my_table_type2; ///< Typdefinition der ersten Registrierungstabelle
#define __REG__(id,fun) (my_table_type2::instance().insert(id,fun)) ///< Makro, um Elemente in Reg. Tab einzufügen
#define __EXEC__(id) (my_table_type2::instance().find(id)()) ///< Makro, um Funktion aufzurufen
static bool dummy ( __REG__(1,fun3) && ///< Registrierung durch statische Initialisierung
__REG__(3,fun2) &&
__REG__(2,fun1) );
int main()
{
typedef register_table<int, fptr_type,1> my_table_type; ///< Typdefinition der zweiten Registrierungstabelle
std::vector<int> cond_v; ///< Tabelle, um Bedingungen zu speichern
typedef register_table<int, fptr_type,2> my_table_type2; ///< Typdefinition der ersten Registrierungstabelle
#define __REG__(id,fun) (my_table_type2::instance().insert(id,fun)) ///< Makro, um Elemente in Reg. Tab einzufügen
#define __EXEC__(id) (my_table_type2::instance().find(id)()) ///< Makro, um Funktion aufzurufen
static bool dummy ( __REG__(1,fun3) && ///< Registrierung durch statische Initialisierung
__REG__(3,fun2) &&
__REG__(2,fun1) );
int main()
{
typedef register_table<int, fptr_type,1> my_table_type; ///< Typdefinition der zweiten Registrierungstabelle
std::vector<int> cond_v; ///< Tabelle, um Bedingungen zu speichern
typedef register_table<int, fptr_type,2> my_table_type2; ///< Typdefinition der ersten Registrierungstabelle
#define __REG__(id,fun) (my_table_type2::instance().insert(id,fun)) ///< Makro, um Elemente in Reg. Tab einzufügen
#define __EXEC__(id) (my_table_type2::instance().find(id)()) ///< Makro, um Funktion aufzurufen
static bool dummy ( __REG__(1,fun3) && ///< Registrierung durch statische Initialisierung
__REG__(3,fun2) &&
__REG__(2,fun1) );
int main()
{
typedef register_table<int, fptr_type,1> my_table_type; ///< Typdefinition der zweiten Registrierungstabelle
std::vector<int> cond_v; ///< Tabelle, um Bedingungen zu speichern
Die Makros __REG__ und __EXEC__ wurden lediglich zur Vereinfachung der Syntax eingeführt. Mit Hilfe der Makros können die Funktionen nun ganz einfach registriert und ausgeführt werden.
Es wird ein Typ-Identifizierer benötigt. Der Datentyp für den Typ-Identifizierer kann frei gewählt werden.
Zu jeder Bedingung muss eine Funktion geschrieben werden (Forced by Design).
Die Registrierungsklasse muss nichts über die zu registrierenden Funktionen wissen (Compiler-Abhängigkeiten).
Abläufe können einfach geändert werden (z.B. Abändern der Einträge von cond_v).
Gute Performance, da der Container std::map verwendet wird, der ein schnelles Suchen ermöglicht.
Der Container std::map benötigt mehr Speicher als ein einfaches Array.
Alle Elemente müssen dynamisch (zur Laufzeit) in den Container eingefügt werden.
6. Typschaltung polymorpher Elementfunktionen
Schauen wir uns noch ein weiteres Anwendungsbeispiel an. Diesmal verwende ich Funktoren aus der Loki-Bibliothek, um Elementfunktionen von polymorphen Objekten aufzurufen. Hauptaufgabe des Funktors ist die Speicherung von aufrufbaren Entitäten. Wie die Funktoren der Loki-Bibliothek im Detail funktionieren, möchte ich hier aber nicht erläutern, da dieses Thema Stoff für einen eigenständigen Artikel böte. Im Buch Modern C++ werden Funktoren sehr ausführlich beschrieben. Wenn ihr das Beispiel nachprogrammieren möchtet, könnt ihr die Loki hier downloaden. Ihr müsst dann entweder die Units smallobj.cpp und singleton.cpp aus der Loki-Bibliothek oder die Loki.lib zu eurem Projekt dazulinken.
Die Ausgabe liefert wieder das erwartete Ergebnis.
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
ctor Base
ctor Base
ctor Derived
ctor Base
ctor AnotherDerived
----------------------------------------
call Base::do_smth()
call Derived::do_smth()
call AnotherDerived::do_smth()
----------------------------------------
dtor AnotherDerived
dtor Base
dtor Derived
dtor Base
dtor Base
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
ctor Base
ctor Base
ctor Derived
ctor Base
ctor AnotherDerived
----------------------------------------
call Base::do_smth()
call Derived::do_smth()
call AnotherDerived::do_smth()
----------------------------------------
dtor AnotherDerived
dtor Base
dtor Derived
dtor Base
dtor Base
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
ctor Base
ctor Base
ctor Derived
ctor Base
ctor AnotherDerived
----------------------------------------
call Base::do_smth()
call Derived::do_smth()
call AnotherDerived::do_smth()
----------------------------------------
dtor AnotherDerived
dtor Base
dtor Derived
dtor Base
dtor Base
7. Erzeugung polymorpher Objekte
Kommen wir nun zu einem letzten ausführlicheren Beispiel. Wie eingangs erwähnt können wir mit dem Object-Switching-Pattern auch eine Objekt-Fabrik implementieren. Es sollen Grafikobjekte aus einer Datei eingelesen und am Bildschirm dargestellt werden. Solche Problemstellungen werden nur selten in C++-Büchern abgehandelt, da sie etwas ausführlicher diskutiert werden müssen. Da es in diesem Artikel aber um dieses Thema geht, stellen wir uns dem Problem. Entwerfen wir also eine einfache Hierarchie von Grafikobjekten. Zunächst benötigen wir einige einfache Datentypen.
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
1 2 3 4 5 6 7 8 9 10 11 12 13 14
//---------------------------------------------------------------------------------------------- struct point
{
int x;
int y;
};
//---------------------------------------------------------------------------------------------- struct gui_elem_file_s
{
int id; ///< Identifizierer
point origin; ///< Ursprungs-Koordinaten const char * data; ///< inhomogene Daten
};
#endif
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
//---------------------------------------------------------------------------------------------- struct point
{
int x;
int y;
};
//---------------------------------------------------------------------------------------------- struct gui_elem_file_s
{
int id; ///< Identifizierer
point origin; ///< Ursprungs-Koordinaten const char * data; ///< inhomogene Daten
};
#endif
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
//---------------------------------------------------------------------------------------------- struct point
{
int x;
int y;
};
//---------------------------------------------------------------------------------------------- struct gui_elem_file_s
{
int id; ///< Identifizierer
point origin; ///< Ursprungs-Koordinaten const char * data; ///< inhomogene Daten
};
#endif
Die Struktur point wird zum Speichern von Koordinaten benötigt. gui_elem_file_s ist eine Struktur, die in binären Files gespeichert wird. Im Element data werden die objektspezifischen Daten abgelegt.
Als Basisklasse für alle Grafikobjekte dient die Klasse Shape. Sie ist nicht vollständig beschrieben, da sie nur die Elemente enthält, die für das Einlesen und Ausgeben von Shape-Objekten relevant sind.
Um die Struktur point vorwärts deklarieren zu können, wird die Membervariable origin_ als Pointer implementiert. Aus dieser Entscheidung resultiert, dass wir den Konstruktor, den Copy-Konstruktor sowie den Zuweisungsoperator per Hand implementieren müssen. Die meisten Designer bevorzugen es, virtuelle Elementfunktionen privat zu deklarieren. Aus diesem Grund werden die öffentlichen, nicht virtuellen Elementfunktionen display_data und parse_data definiert. Die NVI-Elementfunktionen sind Wrapper für die privaten polymorphen Elementfunktionen. Diese Technik wird häufig als Nicht Virtuelles Interface (NVI) bezeichnet.
Bem.: Implementierung der NVI-Elementfunktion display_data. Sie klammert die privaten virtuellen Elementfunktionen whoami und display. Hätten wir nicht auf diese Technik zurückgegriffen, müssten wir die Ausgabe der Koordinaten in jede virtuelle Elementfunktion implementieren.
Definieren wir nun noch einige von Shape abgeleitete Klassen. Ein Kreis ist sicherlich ein sinnvolles Zeichenobjekt. Jede von Shape abgeleitete Klasse definiert eine ID, die zum Identifizieren der Klasse dient.
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12
1 2 3 4 5 6 7 8 9 10 11 12
#include <string>
#include "Shape.h"
//---------------------------------------------------------------------------------------------- class Circle : public Shape
{
public:
enum {ID = 0};
private:
int radius_;
//...
};
//----------------------------------------------------------------------------------------------
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12
#include <string>
#include "Shape.h"
//---------------------------------------------------------------------------------------------- class Circle : public Shape
{
public:
enum {ID = 0};
private:
int radius_;
//...
};
//----------------------------------------------------------------------------------------------
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12
#include <string>
#include "Shape.h"
//---------------------------------------------------------------------------------------------- class Circle : public Shape
{
public:
enum {ID = 0};
private:
int radius_;
//...
};
//----------------------------------------------------------------------------------------------
Die Implementierungsdatei circle.cpp birgt keine Überraschungen mehr. Das Einzige, auf das es sich hier nochmals hinzuweisen lohnt, ist die Initialisierung der statischen Variablen dummy mit der Registrierungsfunktion.
C/C++ Code:
#include "circle.h"
//.. weitere Includes
static bool dummy(Reg<Circle>()); //Registrierung von Circle
//.. Implementierungsdetails
C/C++ Code:
#include "circle.h"
//.. weitere Includes
static bool dummy(Reg<Circle>()); //Registrierung von Circle
//.. Implementierungsdetails
C/C++ Code:
#include "circle.h"
//.. weitere Includes
static bool dummy(Reg<Circle>()); //Registrierung von Circle
//.. Implementierungsdetails
Die Registrierungsfunktion ist als Template implementiert und trägt die ID sowie die dazugehörige Creator-Funktion in die Registrierungstabelle ein. Außerdem müssen wir noch die Typdefinition für die Callback-Funktion und die Registrierungstabelle schreiben. Der Funktor speichert Zeiger auf Funktionen bzw. Methoden, die einen Pointer auf ein Shape-Objekt zurückliefern.
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
//---------------------------------------------------------------------------------------------- typedef Loki::Functor<Shape*> PTMF_callback; //Funktor der Shape-Erzeuger-Funktionen speichert typedef register_table<int, PTMF_callback> reg_tab_polymorph; //Typdefinition der Registrierungstabelle
//---------------------------------------------------------------------------------------------- template <typename Class> bool Reg()
{rators für den Pointer
typedef Class *(*f_ptr)(void);
f_ptr f_ptr_creator(&create<Class>); ///< Angabe des Adressoperators für Templatefunktionen return reg_tab_polymorph::instance().insert(Class::ID,PTMF_callback(f_ptr_creator));
}
//---------------------------------------------------------------------------------------------- template <typename Class>
Class * create()
{
return new Class;
}
//----------------------------------------------------------------------------------------------
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
//---------------------------------------------------------------------------------------------- typedef Loki::Functor<Shape*> PTMF_callback; //Funktor der Shape-Erzeuger-Funktionen speichert typedef register_table<int, PTMF_callback> reg_tab_polymorph; //Typdefinition der Registrierungstabelle
//---------------------------------------------------------------------------------------------- template <typename Class> bool Reg()
{rators für den Pointer
typedef Class *(*f_ptr)(void);
f_ptr f_ptr_creator(&create<Class>); ///< Angabe des Adressoperators für Templatefunktionen return reg_tab_polymorph::instance().insert(Class::ID,PTMF_callback(f_ptr_creator));
}
//---------------------------------------------------------------------------------------------- template <typename Class>
Class * create()
{
return new Class;
}
//----------------------------------------------------------------------------------------------
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
//---------------------------------------------------------------------------------------------- typedef Loki::Functor<Shape*> PTMF_callback; //Funktor der Shape-Erzeuger-Funktionen speichert typedef register_table<int, PTMF_callback> reg_tab_polymorph; //Typdefinition der Registrierungstabelle
//---------------------------------------------------------------------------------------------- template <typename Class> bool Reg()
{rators für den Pointer
typedef Class *(*f_ptr)(void);
f_ptr f_ptr_creator(&create<Class>); ///< Angabe des Adressoperators für Templatefunktionen return reg_tab_polymorph::instance().insert(Class::ID,PTMF_callback(f_ptr_creator));
}
//---------------------------------------------------------------------------------------------- template <typename Class>
Class * create()
{
return new Class;
}
//----------------------------------------------------------------------------------------------
Bem.: Überraschenderweise muss der Adressoperator für den Pointer auf die Template-Funktion angegeben werden. Nicht-Template-Funktionen benötigen ihn nicht.
Bem.: Die Registrierungsfunktion Reg musste ich für den Borland-Compiler umschreiben. Er konnte die Funktion nicht kompilieren, weil der Funktionspointer auf &create<Class> direkt als Argument für den Funktor übergeben wurde.
Die Klasse Rectangle und Square sind ähnlich wie Circle implementiert. Bemerkenswert ist vielleicht, dass Square nicht von Rectangle (Ein Quadrat ist ein Rechteck), sondern von Shape abgeleitet ist. Dadurch wird ein Verstoß gegen das liskovsche Substitutionsprinzip (LSP) vermieden.
Würde Square von Rectangle abgeleitet, könnte dies im Kontext eines Grafikprogramms falsch sein, da mit diesem üblicherweise grafische Elemente verändern werden können. So lässt sich z.B. bei Rechtecken die Länge der beiden Seiten unabhängig voneinander ändern. Für ein Quadrat gilt das jedoch nicht, denn nach einer solchen Änderung wäre es kein Quadrat mehr. Hat also die Klasse Rechteck Methoden wie set_width() oder set_height() (wurde im Beispiel zur besseren Übersicht weggelassen), so erbt Square die Methoden, obwohl deren Anwendung für ein Quadrat nicht erlaubt ist. Das LSP verlangt, dass die Bedeutung von Eigenschaften der Basis-Klassen in abgeleiteten Klassen nicht verändert wird.
Nach soviel Vorbereitung sollten wir nun endlich den Vorhang für unsere Objekt-Fabrik aufmachen. Das Programm ist Gott sei Dank recht einfach gestrickt. Wir können uns also entspannt zurücklehnen. Das Array gui_elem_array[] simuliert die Datei, in der die Daten der Grafikobjekte gespeichert sind. Außerdem definiert das Hauptprogramm noch das Funktionsobjekt DeleteObj, das wir benötigen, um den Speicher für die Objekte mit Hilfe von std::for_each freizugeben.
Kommen wir zum Programmablauf:
Das Array wird geparst. Abhängig von der ID wird eine in der Registrierungstabelle gespeicherte Creator-Funktion aufgerufen. Das dynamisch allokierte Shape-Objekt wird daraufhin im Vektor shape_v gespeichert.
Durch den Aufruf von parse_data werden die objektspezifischen Eigenschaften ermittelt und zugewiesen.
Schließlich müssen die Objekte mit display_data noch ausgegeben werden.
Bevor das Programm beendet wird, dürfen wir es nicht versäumen, die allokierten Grafikobjekte zu zerstören. Wem das nicht gefällt, kann alternativ Smart-Pointer-Objekte verwenden.
#include <iostream>
#include <vector>
#include <algorithm>
#include "shape_types.h"
#include "shape.h"
#include "shape_reg.h"
using namespace std;
using gfx::Shape;
using gfx::gui_elem_file_s;
using gfx::reg_tab_polymorph;
//----------------------------------------------------------------------------------------------
// gui_elem_array[] simuliert Datei, in der die Metainformationen über Grafikobjekte vorliegen. const gui_elem_file_s gui_elem_array[] =
{
{ 0 ,{ 5, 5}, "5" } //Kreis mit Radius 5
,{ 1 ,{10,10}, "7;8" } //Rechteck mit Breite 7 und Höhe 8
,{ 2 ,{20,15}, "3" } //Quadrat mit Seitenlänge 3
,{ 1 ,{35,47}, "13;40"} //Rechteck mit Breite 13 und Höhe 40
,{ 2 ,{60,80}, "99" } //Quadrat mit Seitenlänge 99
};
//----------------------------------------------------------------------------------------------
Shape * Create(int id) ///< gibt registrierte Creator-Funktion auf Grafikobjekt zurück
{
return reg_tab_polymorph::instance().find(id)();
}
//---------------------------------------------------------------------------------------------- struct DeleteObj ///< Funktionsobjekt zum Löschen von dynamisch allokierten Objekten
{
template <typename T> inline void operator() (T * object) const
{
delete object;
}
};
//---------------------------------------------------------------------------------------------- int main()
{
try
{
std::vector<Shape *> shape_v;
std::string data;
for(int idx = 0; idx != sizeof(gui_elem_array)/sizeof(gui_elem_file_s); ++idx)
{
shape_v.push_back(Create(gui_elem_array[idx].id)); ///< Füllt Vector mit Grafikobjekt-Pointer
shape_v[idx]->parse_data(gui_elem_array[idx]); ///< Grafikobjekte mit Daten füllen
cout << shape_v[idx]->display_data(data) << endl; ///< Grafikobjekte ausgeben
}
std::for_each(shape_v.begin(),shape_v.end(),DeleteObj()); ///< alle Grafikobjekt-Pointer löschen
}
catch(std::runtime_error & e)
{
cerr << e.what() << endl;
}
return 0;
}
//----------------------------------------------------------------------------------------------
Bem.: alle Shape-Klassen und Funktionen liegen im Namensraum gfx
Die Ausgabe zeigt die erzeugten Grafikobjekte.
Code:
Circle x = 5 y = 5 radius = 5
Rectangle x = 10 y = 10 width = 7 height = 8
Square x = 20 y = 15 length = 3
Rectangle x = 35 y = 47 width = 13 height = 40
Square x = 60 y = 80 length = 99
Code:
Circle x = 5 y = 5 radius = 5
Rectangle x = 10 y = 10 width = 7 height = 8
Square x = 20 y = 15 length = 3
Rectangle x = 35 y = 47 width = 13 height = 40
Square x = 60 y = 80 length = 99
Code:
Circle x = 5 y = 5 radius = 5
Rectangle x = 10 y = 10 width = 7 height = 8
Square x = 20 y = 15 length = 3
Rectangle x = 35 y = 47 width = 13 height = 40
Square x = 60 y = 80 length = 99
Irgendwie finde ich das Beispiel cool. Obwohl die Header-Dateien für Circle, Rectangle und Square nicht in das main-Programm eingebunden sind, können Kreise, Rechtecke und Quadrate dargestellt werden. Das main-Programm weiß nicht einmal, dass diese Grafikobjekte existieren. Wenn es viele von Shape abgeleitet Klassen gibt, können durch diese Art der losen Kopplung die Zeiten für das Kompilieren eines Projektes drastisch reduziert werden.
8. Schlusswort
Ich hoffe, dass euch das Objekt-Switching-Muster gefallen hat. Vielleicht wandert das Design-Pattern ja in eure Trickkiste. Das Muster ist vielseitig einsetzbar und kann einiges an manueller Kodierungsarbeit ersparen. Die Codestellen habe ich mit dem Visual-C++-Compiler 2005 und dem Borland C++ Builder 6 übersetzt. Sie sollten aber mit jedem standardkonformen C++-Compiler kompiliert werden können. Den vollständigen Source-Code für das Objekt-Fabrik-Beispiel könnt ihr hier als Zip-Archiv downloaden.
9. Referenzen
Effektiv C++ programmieren von Scott Meyers
ISBN: 3827322979
Effective STL von Scott Meyers
ISBN: 0201749629
Modernes C++ Design. Generische Programmierung und Entwurfsmuster angewendet von Andrei Alexandrescu
ISBN: 3826613473
_________________ Erik Griessmann
Zuletzt bearbeitet von GPC am 10:00:33 21.11.2006, insgesamt 5-mal bearbeitet
Haben sie sich das Pattern selbst ausgedacht oder warum findet man darüber in Google nichts?
Jein! Objekt-Fabriken gibt es ja bereits. Ich habe das Muster nur modifiziert, da ich mich nicht auf die Erzeugung von Objekten beschränken wollte.
Den Begriff Object-Switching habe ich mir ausgedacht.
Nächstes Thema anzeigen Vorheriges Thema anzeigen
Sie können keine Beiträge in dieses Forum schreiben. Sie können auf Beiträge in diesem Forum antworten. Sie können Ihre Beiträge in diesem Forum nicht bearbeiten. Sie können Ihre Beiträge in diesem Forum nicht löschen. Sie können an Umfragen in diesem Forum nicht mitmachen.
c++.de ist Teilnehmer des Partnerprogramms von Amazon Europe S.à.r.l. und Partner des Werbeprogramms, das zur Bereitstellung eines Mediums
für Websites konzipiert wurde, mittels dessen durch die Platzierung von Werbeanzeigen und Links zu amazon.de
Werbekostenerstattung verdient werden kann.
Die Vervielfältigung der auf den Seiten www.c-plusplus.de, www.c-plusplus.info, www.c-sar.de, www.c-plusplus.net und www.baeckmann.de
enthaltenen Informationen ohne eine schriftliche Genehmigung des Seitenbetreibers ist untersagt
(vgl. §4 Urheberrechtsgesetz). Die Nutzung und Änderung der vorgestellten Strukturen und Verfahren in
privaten und kommerziellen Softwareanwendungen ist ausdrücklich erlaubt, soweit keine Rechte Dritter verletzt werden.
Der Seitenbetreiber übernimmt keine Gewähr für die Funktion einzelner Beiträge oder Programmfragmente, insbesondere
übernimmt er keine Haftung für eventuelle aus dem Gebrauch entstehenden Folgeschäden.