[X] Object-Switching
-
Und auch noch schoen gleich mit Beispiel ganz mein Geschmack eben
Aber der erste Satz oder mit einer der ersten "auch noch Leute die ... ihren Code nur mit Struktogrammeditoren verstehen".
Ich weiss nicht ich wuerde den weglassen Leute die wirklich so schreiben werden selbst mit besagten Tools Probleme mit Ihrem Code haben, wenn die
500 case in Ihren switch drinhaben.Zum anderen glingt es so oder könnte es als ob Du selbst von jeglichen Designtools ueberhaupt nichts hälst. Was dann wieder so klingen kann
wie ach "der Programmiert ohne Konzept einfach drauflos".Selbst zu Codereviews muss man ja auch vorher zumindest ein kleines Konzept oder
ein Schema haben mit dem man rangeht, wenn man fremden Code ansieht!
-
sclearscreen schrieb:
Aber der erste Satz oder mit einer der ersten "auch noch Leute die ... ihren Code nur mit Struktogrammeditoren verstehen".
Ich weiss nicht ich wuerde den weglassen Leute die wirklich so schreiben werden selbst mit besagten Tools Probleme mit Ihrem Code haben, wenn die
500 case in Ihren switch drinhaben.Zum anderen glingt es so oder könnte es als ob Du selbst von jeglichen Designtools ueberhaupt nichts hälst. Was dann wieder so klingen kann
wie ach "der Programmiert ohne Konzept einfach drauflos".Du hast schon recht. Ich mag diese Tools nicht besonders, weil sie nach meiner Meinung zu schmutzigen Programmieren verführen. Man klickt einfach ein paar if, else, else if, etc. zusammen und alles schaut toll aus. Wenn man sich dann den erzeugten Code anschaut, sieht man nur noch Spagetti-Code. Solche Tools verwenden dann auch noch Ausblendungen um die Komplexität überblicken zu können.
Ich werde den Seitenhieb aus den Artikel entfernen, da dies schon eine recht subjektive Äußerung ist.
-
Hallo,
mir gefällt der Artikel auch sehr gut, nur eins ist mir gerade aufgefallen: Im Titel sprichst du von Object Switching, im folgenden Text von Objekt Switching. Auf eins musst du dich festlegen, ich tendiere persönlich eher zu Object Switching...wenn schon Englisch, dann richtig. Außerdem ist der Term gelegentlich kursiv und manchmal nicht.
Sonst ist alles im Grünen Bereich, denk bitte auch an dein Profil, siehe hier. Danke
MfG
GPC
-
Ich habe noch mal über das letzte Beispiel nachgedacht. Das ist wohl ein bißchen daneben.
Damit kann man ganz toll Resourcelecks erzeugen. Außerdem ist der Sinn von Funktoren ein anderer.
Ich muss den letzten Teil also noch abändern.
Soll ich den Artikel wieder auf [F] oder [A] setzten?
-
rik schrieb:
Soll ich den Artikel wieder auf [F] oder [A] setzten?
gehupft wie gesprungen ... nimm [F]
-
Habe den alten Krempel mal entfernt
-
Wir haben im Prinzip keine Beschränkung der Länge, kommt er die aber zu lang vor, splitte ihn auf.
MfG
GPC
-
GPC schrieb:
Wir haben im Prinzip keine Beschränkung der Länge, kommt er die aber zu lang vor, splitte ihn auf.
...sofern dies möglich ist, ohne ihn zu "zerreißen".
-
Habe Artikel verschoben
-
Spricht etwas dagegen Source-Code als ZIP-Datei upzuloaden?
-
Inhalt
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. Referenzen1. Einleitung
Manchmal ist ein C- oder C++-Programmierer in der bedauernswerten Situation, sich mit ellenlangen switch-Anweisungen der Form
//... 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.//... 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:
typedef void (*fptr)(void); ///< Definition eines Funktionspointers typedef struct fptr_tab_s ///< Definition einer Struktur um Bedingung und Funktion zu verknüpfen { int condition; fptr func; } fptr_tab; #define CONDITION_1 2042 #define CONDITION_2 1012 //... #define CONDITION_n 5211 fptr_tab array_fptr[] = { {CONDITION_1, function_1}, ///< der Bedingung Condition_1 wird die Funktion function_1 zugeordnet. {CONDITION_2, function_2}, //.... {CONDITION_n, function_n} }; #define MAX_COUNT_ARRAY_FPTR sizeof(array_fptr)/sizeof(fptr_tab) int array_condition[] = { CONDITION_3, CONDITION_2, CONDITION_9, //... CONDITION_8, //... }; #define MAX_COUNT_ARRAY_CONDTION sizeof(array_condition)/sizeof(int) //... for(k = 0; k != MAX_COUNT_ARRAY_CONDITION; ++k) for(j = 0; j != MAX_COUNT_ARRAY_FPTR; ++j) if(array_fptr[j].condition == array_condition[k]) array_fptr[j].func(); //Funktion aufrufen //...
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.
void insert_into_map(void) { typedef std::map<int,std::string> s_map; bool success; s_map string_map; success = string_map.insert(s_map::value_type(1,"Hallo")).second; if(success) success = string_map.insert(s_map::value_type(2,"OK")).second; //success = true success = string_map.insert(s_map::value_type(2,"Not OK")).second; //success = false }
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.
//.. typedef std::map<int,std::string> s_map; bool success(false); s_map string_map; int key(1); //... success = (string_map.erase(key) == 1); //...
Bem.: Die Member-Funktion erase gibt die Anzahl der gelöschten Elemente zurück.
Das Suchen von Elementen erfolgt mit der Member-Funktion find, die einen Iterator zurückgibt.
//.. typedef std::map<int,std::string> s_map; s_map string_map; //... const std::string & get_value(int key) { s_map::const_iterator s_iter = string_map.find(key); if(s_iter == string_map.end()) { throw std::runtime_error("key not found"); } return s_iter.second; } //...
Bem.: Falls das gesuchte Element nicht gefunden werden kann, wird eine Exception vom Typ std::runtime_error ausgelöst.
Mit diesem Wissen können wir eine Templateklasse schreiben, welches die Funktionalitäten von std::map auf die benötigten Elemente einschränkt.
#include <map> template <typename Key, typename Data> class register_table { public: typedef Key key_type; typedef Data data_type; typedef std::map<key_type,data_type> table_type; typedef typename table_type::value_type value_type; typedef typename table_type::iterator iterator; typedef typename table_type::const_iterator const_iterator; private: table_type callback_map; public: bool erase(key_type key) { return (callback_map.erase(key) == 1); } bool insert(key_type key, data_type data) { return callback_map.insert(value_type(key,data)).second; } data_type find(key_type key) const { const_iterator cb_iter(callback_map.find(key)); if(cb_iter == callback_map.end()) throw std::runtime_error("key not found"); return cb_iter->second; } };
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.
#include <iostream> #include <vector> #include "register_table.h" using namespace std; void fun1(void) { cout << "fun1 " << endl; } void fun2(void) { cout << "fun2 " << endl; } void fun3(void) { cout << "fun3 " << endl; } int main() { typedef void (*fptr_type) (void); typedef register_table<int, fptr_type> my_table_type; my_table_type my_table; std::vector<int> cond_v; cond_v.push_back(2); cond_v.push_back(1); cond_v.push_back(3); cond_v.push_back(2); cond_v.push_back(2); cond_v.push_back(1); cond_v.push_back(3); my_table.insert (1,fun1); my_table.insert (2,fun2); my_table.insert (3,fun3); try { for (size_t idx = 0 ; idx != cond_v.size(); ++idx) my_table.find(cond_v[idx])(); } catch(std::runtime_error & e) { cout << e.what() << endl; } return 0; }
Die Ausgabe liefert das erwartete Ergebnis
fun2 fun1 fun3 fun2 fun2 fun1 fun3
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.
struct DefaultError { static void process_error() { throw std::runtime_error("key not found"); } };
Da die Methode process_error() statisch ist, müssen keine Objekte erzeugt werden.
template <typename Key, typename Data, typename OnError=DefaultError> class register_table { //.. data_type find(key_type key) const { const_iterator cb_iter = callback_map.find(key); if(cb_iter == callback_map.end()) OnError::process_error(); return cb_iter->second; //... }
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.
struct IgnoreError { static void process_error() {} //do nothing }; //... typedef register_table<int,fptr_type,IgnoreError> my_table_type;
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.
Singleton & instance() { static Singleton obj; return obj; }
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.// my_function.cpp #include "register_table.h" static bool dummy( instance().insert(1,fun1) &&instance().insert(2,fun2) &&instance().insert(3,fun3) ); void fun1() {...} void fun2() {...} void fun3() {...}
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.
template <typename Key, typename Data, int Inst = 0, typename OnError=DefaultError> class register_table { //... } //... typedef register_table<int, fptr_type> my_table_type1; //1. Instanz (Dummy-Template-Parameter) typedef register_table<int, fptr_type,1> my_table_type2; //2. Instanz typedef register_table<int, fptr_type,2> my_table_type3; //3. Instanz
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.
#ifndef __register_table_4_02_05_2006__ #define __register_table_4_02_05_2006__ //---------------------------------------------------------------------------------------------- #include <map> #include "default_policy.h" //---------------------------------------------------------------------------------------------- template <typename Key, typename Data, int Inst = 0, typename OnError=DefaultError> class register_table { public: typedef Key key_type; typedef Data data_type; typedef std::map<key_type,data_type> table_type; typedef typename table_type::value_type value_type; typedef typename table_type::iterator iterator; typedef typename table_type::const_iterator const_iterator; private: table_type callback_map; register_table(){} register_table(const register_table &); //Deaktiviere Copy-Ctor register_table & operator=(const register_table &); public: ~register_table(){} //---------------------------------------------------------------------------------------------- bool erase(key_type key) { return (callback_map.erase(key) == 1); } //---------------------------------------------------------------------------------------------- bool insert(key_type key, data_type data) { return callback_map.insert(value_type (key,data)).second; } //---------------------------------------------------------------------------------------------- data_type find(key_type key) const { const_iterator cb_iter(callback_map.find(key)); if(cb_iter == callback_map.end()) OnError::process_error(); return cb_iter->second; } //---------------------------------------------------------------------------------------------- static register_table<Key,Data,Inst,OnError> & instance() { static register_table<Key,Data,Inst,OnError> rt; return rt; } }; //---------------------------------------------------------------------------------------------- #endif
5. Typschaltung globaler Funktionen
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.
#include <iostream> #include <vector> #include "register_table2.h" using namespace std; void fun1(void) { cout << "fun1 " << endl; } void fun2(void) { cout << "fun2 " << endl; } void fun3(void) { cout << "fun3 " << endl; } typedef void (*fptr_type) (void); 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 cond_v.push_back(2); cond_v.push_back(1); cond_v.push_back(3); cond_v.push_back(2); cond_v.push_back(2); cond_v.push_back(1); cond_v.push_back(3); my_table_type::instance().insert (1,fun1); ///< Registrierungen zur Programmlaufzeit my_table_type::instance().insert (2,fun2); my_table_type::instance().insert (3,fun3); try { for (size_t idx = 0 ; idx != cond_v.size(); ++idx) my_table_type::instance().find(cond_v[idx])(); ///< Syntax wirkt wahrscheinlich eher abschreckend cout << "-----------------------------" << endl; for (size_t idx = 0 ; idx != cond_v.size(); ++idx) __EXEC__(cond_v[idx]); ///< einfach anzuwenden } catch(std::runtime_error & e) { cout << e.what() << endl; } return 0; }
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.
Die Ausgabe liefert das erwartete Ergebnis
fun2 fun1 fun3 fun2 fun2 fun1 fun3 ----------------------------- fun1 fun3 fun2 fun1 fun1 fun3 fun2
Unser Modul hat folgende Eigenschaften:
- 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.
#include <iostream> #include <vector> #include <string> #include "register_table3.h" #include <Loki/Functor.h> using namespace std; //---------------------------------------------------------------------------------------------- class Base { public: Base() {cout << "ctor Base" << endl;} virtual void do_smth() const {cout << "call Base::do_smth()" << endl;} virtual ~Base() {cout << "dtor Base" << endl;} }; //---------------------------------------------------------------------------------------------- class Derived : public Base { public: Derived() {cout << "ctor Derived" << endl;} virtual void do_smth() const {cout << "call Derived::do_smth()" << endl;} virtual ~Derived() {cout << "dtor Derived" << endl;} }; //---------------------------------------------------------------------------------------------- class AnotherDerived : public Base { public: AnotherDerived() {cout << "ctor AnotherDerived" << endl;} virtual void do_smth() const {cout << "call AnotherDerived::do_smth()" << endl;} virtual ~AnotherDerived() {cout << "dtor AnotherDerived" << endl;} }; //---------------------------------------------------------------------------------------------- template <typename Class> Class * instance() ///< schon wieder ein Singleton Pattern :-) { static Class obj; return &obj; } //---------------------------------------------------------------------------------------------- // 1. Param -> Returntyp der Memberfunktion; // 2. Param (optional) Typliste mit Aufrufparametern der Memberfunktion; typedef Loki::Functor<void> PTMF_callback; typedef register_table<int, PTMF_callback> reg_tab_polymorph; ///< Typdefinition der Registrierungstabelle //---------------------------------------------------------------------------------------------- template <typename Class> bool Register(int id) { return reg_tab_polymorph::instance().insert(id,PTMF_callback(instance<Class>(),&Class::do_smth)); } //---------------------------------------------------------------------------------------------- void Execute(int id) { reg_tab_polymorph::instance().find(id)(); } static bool dummy( Register<Base>(0) ///< Registrierung durch statische Initialisierung && Register<Derived>(1) && Register<AnotherDerived>(2) ); //---------------------------------------------------------------------------------------------- int main() { try { cout << std::string(40,'-') << endl; for (int idx = 0 ; idx != 3; ++idx) Execute(idx); cout << std::string(40,'-') << endl; } catch(std::runtime_error & e) { cout << e.what() << endl; } return 0; } //----------------------------------------------------------------------------------------------
Die Ausgabe liefert wieder das erwartete Ergebnis.
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.
//---------------------------------------------------------------------------------------------- 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.
#ifndef __shape_04_06_2006_hdr__ #define __shape_04_06_2006_hdr__ #include <string> //---------------------------------------------------------------------------------------------- struct gui_elem_file_s; ///< Vorwärtsdeklarationen struct point; //---------------------------------------------------------------------------------------------- class Shape { protected: point * origin_; ///< Pointer auf Ursprungskoordinaten private: virtual void parse(const std::string & buffer) = 0; ///< Objekt einlesen virtual std::string & display(std::string & str) const = 0; ///< Objekt ausgeben virtual std::string & whoami(std::string & str) const = 0; ///< Wer bin ich public: Shape(); ///< ctor Shape(const Shape & obj); ///< copy ctor Shape & operator=(const Shape & obj); ///< Zuweisungsoperator std::string & display_data(std::string & str) const; ///< NVI-Funktion void parse_data(const gui_elem_file_s & buffer); ///< NVI-Funktion virtual ~Shape(); }; //---------------------------------------------------------------------------------------------- #endif
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 händisch 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.
std::string & Shape::display_data(std::string & str) const { std::string tmp; return whoami(str) .append(" x = ") .append(boost::lexical_cast<std::string>(origin_->x)) .append(" y = ") .append(boost::lexical_cast<std::string>(origin_->y)) .append(" ") .append(display(tmp)); }
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.
#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.
#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.
//---------------------------------------------------------------------------------------------- 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 das Objekt 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.
Circle x = 5 y = 5 radius = 0 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 | ISBN: 3827322979
Effektiv C++ programmieren von Scott Meyers
ISBN: 3827322979Effective STL | ISBN: 0201749629
Effective STL von Scott Meyers
ISBN: 0201749629Modernes C++ Design | ISBN: 3826613473
Modernes C++ Design. Generische Programmierung und Entwurfsmuster angewendet von Andrei Alexandrescu
ISBN: 3826613473
-
Beim Punkt 7 nach dem zweiten Codebeispiel das Wort "händisch". Meinst du damit "per Hand"? Dann schreibs gleich hin, denn dann gibts "händisch" nicht. Zudem hört es sich auch komisch an.
Du solltest es allerdings lassen, falls es sich um einen Fachbegriff handelt, was ich ja nicht glaube...
Mr. B