Twitter und Facebook-Anbindung
X
Tweet Follow @twitterapi
!!! Anbindung an twitter und facebook öffnen !!!

Wenn Ihnen mein Online-Buch gefällt,
dann bedanken Sie sich doch mit einer kleinen Spende...

H Design Pattern

H Design Pattern

Im Bereich der Programmierung gibt es Aufgabenstellungen bzw. Szenarien, welche immer wieder auftreten. Damit nicht immer das Rad neu erfunden werden muss, haben sich ein paar kluge Köpfe hingesetzt und geniale Lösungen entwickelt. Diese nennt man Design Pattern oder Entwurfsmuster. Es handelt sich also um eine Art bewährter Bauplan / Herangehensweisen, welche grade für größere Projekte gedacht sind.

Die Verwendung dieser Pattern kann ich Ihnen nur ans Herz legen. Sie sparen nicht nur enorm Zeit und Nerven, sondern können sich sicher sein, eine perfekte Lösung zu haben. Zudem werden sich andere Programmierer in Ihrem Quelltext besser zurecht finden können, da Ihnen diese Pattern auch geläufig sind und sie somit wissen, auf was sie sich verlassen können.

In diesem Kapitel werde ich einige Pattern aufzählen, aber nur die geläufigsten näher betrachten. Zudem empfehle ich Ihnen, dieses Thema bei bedarf im Internet nachzurecherchieren. Es gibt unzählige Bücher, welche sich nur mit Software Pattern beschäftigen, da diese Thematik durchaus komplex ist.

Zum Seitenanfang
Zum Inhaltsverzeichnis

36 Weitere Muster

36 Weitere Muster

coming soon...

Zum Seitenanfang
Zum Inhaltsverzeichnis


33 Erzeugende Muster

33 Erzeugende Muster

Wie Sie anhand der Überschriften erkennen können, lassen sich diese Muster in Kategorien einteilen. Die erste Gruppe beschäftigt sich mit dem Erzeugen von Produkten, wobei der Begriff Produkt ein Synonym ist. Zum einen geht es darum Objekte zu erstellen, aber auch darum, Ergebnisse wie Ausgabedateien zu erzeugen. Wann immer es also darum geht größere Objekte oder verschiedenartige Ausgabeformate zu produzieren, können Sie sich diese Muster als Vorlage heranziehen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

33.1 Singleton

33.1 Singleton

Ein Singleton ist eine Klasse, von welcher man programmweit nur ein Instanz erzeugen kann. In der Regel steht es dann global zur Verfügung und kann somit von überall verwendet werden. Doch wofür braucht man so etwas? Man entscheidet sich immer für eine Singletonklasse, wenn deren Erzeugung teuer ist (zeitaufwändig und oder speicherintensiv). Normalerweise handelt es sich dann um Klassen, welche zentrale Aufgaben übernehmen und somit theoretisch auch nur einmal benötigt werden.

Doch wie erreicht man es, dass man nur eine Instanz einer Klasse erzeugen kann? Die Grundidee ist, dass man das Objekt nicht an einer beliebigen Stelle erzeugt (es sogar verbietet), sondern in einer statischen Klassenmethode, welche gleichzeitig kontrolliert, dass es eben nur eine Instanz gibt. Die Instanz selbst, wird auch in einer statischen Variable gespeichert.

Vereinfachte Struktur eines Singleton

Wie verbietet man nun, dass man eine Instanz erzeugen bzw. freigeben kann? Hierfür muss man nur die Konstruktoren, gewisse Operatoren und den Destruktor als privat deklarieren. Die statische Klassenmethode darf auf die Konstruktoren zugreifen, aber nicht die Außenwelt.

Um Ihnen dies zu demonstrieren, habe ich mir ein kleines Beispiel einfallen lassen, welches schematisch einen Datenbankclient repräsentiert. Da man in der Regel eine zentrale Verbindung zur Datenbank hat, kann das Verbindungsobjekt von einer Singletonklasse sein und alle internen Module können gemeinsam diese Verbindung nutzen.

Möglicher Aufbau eines Datenbankclients

Aus Gründen der Übersichtlichkeit, konzentriere ich mich jetzt nur auf die Definition und Implementierung der Singletonklasse. Schauen Sie sich zunächst die Header-Datei an.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
					
// Klasse zur Kapselung einer Datenbankverbindsung
class CSingletonDBConnection {
	private:
		static CSingletonDBConnection* s_pConnection;

		// Standardkonstruktor – Erzeugung von außen verhindern
		CSingletonDBConnection() {}
		// Kopierkonstruktor – Erzeugung von außen verhindern
		CSingletonDBConnection(const CSingletonDBConnection&) {}
		// Zuweisungsoperator – Erzeugung von außen verhindern
		CSingletonDBConnection& operator=(const CSingletonDBConnection&) {}
		// Destruktor – Freigeben von außen verhindern
		~CSingletonDBConnection() {}

	public:
		// Ggf. Objekt erzeugen und Referenz zurückgeben
		static CSingletonDBConnection* GetInstance();
		// Kontrolliertes Löschen
		static void Destroy();
};
					

Wie Sie sehen, habe ich eine statische Klassenvariable, welche später auf die eine erzeugte Instanz verweisen wird. Zudem habe ich die Konstruktoren und den Zuweisungsoperator privat deklariert. Da ich Ihnen, wie gesagt, nur das Prinzip zeigen möchte, haben diese Methoden keine weitere Logik.

Im öffentlichen Bereich gibt es jetzt zwei statische Methoden. Normalerweise reicht die Methode "GetInstance", aber die Methode "Destroy" ist ganz praktisch, da ich so entscheiden kann, wann das Objekt freigegeben wird. Würde ich mir nicht einen solchen Mechanismus bauen, würde das Objekt irgendwann bei Programmende freigegeben werden. Wann genau, kann man aber nicht sagen. So habe ich volle Kontrolle über meine Datenbankverbindung. Auch hier müsste ich eigentlich noch solche Informationen wie Verbindungsdaten mit übergeben, aber aus Gründen der Einfachheit, habe ich mir das an dieser Stelle auch gespart.

Schauen Sie sich als nächstes die Implementierung der zwei statischen Klassenmethoden in der CPP-Datei an.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
					
#include <stdlib.h>
#include "SingletonDBConnection.h"

CSingletonDBConnection* CSingletonDBConnection::s_pConnection = NULL;



// Ggf. Objekt erzeugen und Referenz zurückgeben //////////////////////////////
CSingletonDBConnection* CSingletonDBConnection::GetInstance() {
	// Wenn die Verbindung noch nicht erzeugt wurde
	if (s_pConnection == NULL) s_pConnection = new CSingletonDBConnection();

	return s_pConnection;
} // GetInstance //////////////////////////////////////////////////////////////



// Kontrolliertes Löschen /////////////////////////////////////////////////////
void CSingletonDBConnection::Destroy() {
	// Nur wenn schon eine Instanz erzeugt wurde
	if (s_pConnection != NULL) delete s_pConnection;

	s_pConnection = NULL;
} // Destroy //////////////////////////////////////////////////////////////////
					

Was hier passiert, ist eigentlich keine große Magie. Zu Beginn, in Zeile 4, initialisiere ich das statische Klassenattribut. Diese Schreibweise sollte Ihnen ja schon bekannt sein. In den zwei Methoden prüfe ich jetzt nur, ob die Instanz schon erzeugt bzw. freigegeben wurde und wenn dem nicht so ist, sorge ich dafür, dass dies geschieht. Ansonsten wird nur ein Zeiger auf die Instanz zurückgegeben bzw. das statische Klassenattribut zurückgesetzt.

Der Vollständigkeit halber, folgt nun die Verwendung dieser Klasse von außen.

 1
 2
 3
 4
 5
 6
					
CSingletonDBConnection* pConnection	= CSingletonDBConnection::GetInstance();

// ...

CSingletonDBConnection::Destroy();
pConnection				= NULL;
					

Wann immer Sie also etwas von "GetInstance" lesen, können Sie sich fast sicher sein, dass das Singleton Pattern zum Einsatz kommt und welche Folgen sich daraus ergeben.

Zum Seitenanfang
Zum Inhaltsverzeichnis

33.3 Abstract Factory

33.3 Abstract Factory

Das Entwurfsmuster der abstrakten Fabriken kapselt wieder die Implementierung und Erzeugung von Produkten, aber im Gegensatz zu den Fabrikmethoden, steht dieses mal nicht das Produkt als Einzelnes im Vordergrund, sondern eine Produktfamilie. Mit Familie ist ein ganzes Sortiment an Produkten gemeint. Die Herangehensweise ist entsprechend auch eine andere und zudem wesentlich komplexer.

Das Grundprinzip und somit auch das Einsatzfeld, ist folgendes. Man hat eine Anzahl von unterschiedlichen Produkten, welche nichts miteinander zu tun haben (also keine gemeinsame Basisklasse besitzen). Für die Erzeugung schafft man sich wieder eine Fabrik. Jetzt möchte man genau diese Produkte auf eine andere Art und Weise erzeugen. Hierfür baut man sich eine zweite Fabrik. Beide Fabriken erzeugen also ähnliche Produkte und somit schafft man ein Interface für die Fabriken.

Vereinfachte Struktur des Abstract Factory Pattern

Ein konkretes Beispiel wären die Firmen Siemens und AEG. Jene stellen Waschmaschinen und Geschirrspüler her. Abgesehen davon, dass diese beiden Produkte Küchengeräte sind, haben Sie nicht viel gemeinsam. Wir gehen also mal davon aus, dass sie von keiner gemeinsamen Klasse erben. Waschmaschine und Geschirrspüler sind jetzt wieder Abstrakte Produkte. Eine Fabrik, die Waschmaschinen und Geschirrspüler produzieren kann, ist eine abstrakte Fabrik. Das Werk von Siemens und das Werk von AEG sind jetzt konkrete Fabriken und jede Firma stellt jetzt seine eigene Variante seiner Geräte her. Die Grundfunktionalität bleibt erhalten, nämlich Wäsche waschen bzw. Geschirr spülen. Dennoch entscheidet jedes konkrete Produkt selber, wie viel Wasser und Strom benötigt wird.

Sie sehen also, dass es sich dieses mal um eine komplexere Struktur handelt. Dieses Pattern wird häufig für Frameworks benutzt, wobei man flexibel bei der Implementierung der Produkte sein möchte. Man kann also im Nachhinein nicht nur die Produkte ändern oder Austauschen, sondern auch den Prozess ihrer Erzeugung und Verwaltung. In dem von mir gewählten Beispiel wird es darum gehen, Autos und Motorräder zu erzeugen, wobei sie einmal optimiert sein sollen für OpenGL und einmal für DirectX. Es wird also eine abstrakte Fabrik geben, welche ein Interface darstellt und sagt, dass eine Fabrik Autos und Motorräder erzeugen kann. Davon abgeleitet werden dann konkrete Fabriken, welche sich um OpenGL Models und DirectX Models kümmern. Da diese Struktur etwas umfangreicher ist, hier zunächst ein Überblick aller Dateien, wobei die gestrichelten Linien zeigen sollen, wer was inkludiert.

Überblick der Dateistruktur des Abstract Factory Beispieles

Sie sehen, dass der Client jetzt nur beide Fabriken inkludiert und somit auch die abstrakte Fabrik und die abstrakten Produkte kennt. Alle konkreten Produkte und alle konkreten Fabriken können also im Nachhinein modifiziert werden, ohne dass der Client und alle nicht modifizierten Produkte und Fabriken, neu kompiliert werden müssen. Einzige Bedingung ist wieder, dass ihre Schnittstelle nicht modifiziert wird.

Zu Beginn definiere ich mir die abstrakten Klassen, also die Schnittstellen.

Auto.h:

 1
 2
 3
 4
 5
					
// Schnittstelle für ein Auto
class IAuto {
	public:
		virtual void AutoAusgabe() = 0;
};
					

Motorrad.h:

 1
 2
 3
 4
 5
					
// Schnittstelle für ein Motorrad
class IMotorrad {
	public:
		virtual void MotorradAusgabe() = 0;
};
					

AbstractFactory.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
					
#include "Auto.h"
#include "Motorrad.h"

// Schnittstelöle für eine Fabrik
class IAbstractFactory {
	public:
		virtual IAuto*		CreateAutoInstance()			= 0;
		virtual IMotorrad*	CreateMotorradInstance()		= 0;

		virtual void		ReleaseAutoInstance(IAuto*&)		= 0;
		virtual void		ReleaseMotorradInstance(IMotorrad*&)	= 0;
};
					

Sie sehen anhand der Schnittstelle für Auto und Motorrad, dass beide nicht viel können, aber das was sie können, unterscheidet sich. Interessanter ist jetzt das Interface der Fabrik. Hier sollten gleich zwei Sachen ins Auge fallen. Zum einen habe ich jetzt für jedes Produkt ein Methodenpaar zum Erzeugen und Freigeben. Der Standard sieht also vor, dass man hier keinen Parameter übergibt, welcher regelt, welches Produkt erzeugt werden soll. Sicher hätte man dies auch so machen können, aber da dies die Sache nicht einfacher macht und zudem vom Standard abweicht, habe ich darauf verzichtet.

Als zweites sollte auffallen, dass die Methoden nicht mehr statisch sind. Zum Erzeugen der Produkte benötigt man also später eine Instanz der konkreten Fabrik. In anderen Sprachen, wie z.B. Java, kann man diese Methoden durchaus statisch definieren, aber C++ verweigert dies. Da man also in C++ eine Instanz benötigt, ist es oft üblich, diese mit Hilfe des Singleton Pattern zu Implementieren. Da dies die Sache auch nicht einfacher macht, habe ich ebenfalls auf diesen Mechanismus verzichtet.

Schauen Sie sich als nächstes die OpenGL Produkte an.

MercedesGL.h:

 1
 2
 3
 4
 5
 6
 7
					
#include "Auto.h"

// Mercedesklasse auf Basis von OpenGL
class CMercedesGL : public IAuto {
	public:
		virtual void AutoAusgabe();
};
					

MercedesGL.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
					
#include "MercedesGL.h"



// Spezielle Ausgabe auf der Konsole //////////////////////////////////////////
void CMercedesGL::AutoAusgabe(void) {
	printf("Mercedes mit OpenGL!\n");
} // AutoAusgabe //////////////////////////////////////////////////////////////
					

DukatiGL.h:

 1
 2
 3
 4
 5
 6
 7
					
#include "Motorrad.h"

// Dukatiklasse auf Basis von OpenGL
class CDukatiGL : public IMotorrad {
	public:
		virtual void MotorradAusgabe();
};
					

DukatiGL.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
					
#include "MercedesGL.h"



// Spezielle Ausgabe auf der Konsole //////////////////////////////////////////
void CDukatiGL::MotorradAusgabe(void) {
	printf("Dukati mit OpenGL!\n");
} // MotorradAusgabe //////////////////////////////////////////////////////////
					

Wie Sie sehen, gibt es auch hier nicht viel Magie. Die Definition und Implementierung ist sehr schlicht gehalten. Ich implementiere lediglich die abstrakte Methode und lasse auch da nur eine Zeile auf der Konsole ausgeben.

Genau das gleiche Prinzip wende ich in der DirectX Familie an. Es wird sich also lediglich die Ausgabe ändern.

MercedesDX.h:

 1
 2
 3
 4
 5
 6
 7
					
#include "Auto.h"

// Mercedesklasse auf Basis von DirectX
class CMercedesDX : public IAuto {
	public:
		virtual void AutoAusgabe();
};
					

MercedesDX.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
					
#include "MercedesDX.h"



// Spezielle Ausgabe auf der Konsole //////////////////////////////////////////
void CMercedesDX::AutoAusgabe(void) {
	printf("Mercedes mit DirectX!\n");
} // AutoAusgabe //////////////////////////////////////////////////////////////
					

DukatiDX.h:

 1
 2
 3
 4
 5
 6
 7
					
#include "Motorrad.h"

// Dukatiklasse auf Basis von DirectX
class CDukatiDX : public IMotorrad {
	public:
		virtual void MotorradAusgabe();
};
					

DukatiDX.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
					
#include "MercedesDX.h"



// Spezielle Ausgabe auf der Konsole //////////////////////////////////////////
void CDukatiDX::MotorradAusgabe(void) {
	printf("Dukati mit DirectX!\n");
} // MotorradAusgabe //////////////////////////////////////////////////////////
					

Etwas interessanter wird jetzt die "main".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
					
#include "GLFabrik.h"
#include "DXFabrik.h"



// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main (int argc, char** argv) {
	CGLFabrik	oGLFabrik;
	CDXFabrik	oDXFabrik;

	IAuto*		pMercedesGL	= oGLFabrik.CreateAutoInstance();
	IMotorrad*	pDukatiGL	= oGLFabrik.CreateMotorradInstance();

	IAuto*		pMercedesDX	= oDXFabrik.CreateAutoInstance();
	IMotorrad*	pDukatiDX	= oDXFabrik.CreateMotorradInstance();

	pMercedesGL->AutoAusgabe();
	pDukatiGL->MotorradAusgabe();

	pMercedesDX->AutoAusgabe();
	pDukatiDX->MotorradAusgabe();

	oGLFabrik.ReleaseAutoInstance(pMercedesGL);
	oGLFabrik.ReleaseMotorradInstance(pDukatiGL);

	oDXFabrik.ReleaseAutoInstance(pMercedesDX);
	oDXFabrik.ReleaseMotorradInstance(pDukatiDX);

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

Mercedes mit OpenGL!
Dukati mit OpenGL!
Mercedes mit DirectX!
Dukati mit DirectX!
		

Unterm Strich befindet sich aber auch hier nichts spannendes. Wie Sie bereits wissen, findet man keinen Code mehr zum Erzeugen oder Freigeben, da dies wieder die Fabriken übernehmen. Des weiteren sei noch mal darauf hingewiesen, dass ich mir je eine Instanz pro Fabrik erzeuge.

Sie sehen also, dass die Implementierung dieses Entwurfsmustern gar nicht mal so schwer ist. Jedoch ist es aufwändig, da man viele Klassen und somit Dateien benötigt. Leider darf man hier keine Inlinemethoden benutzen, da man sonst nicht mehr ganz so flexibel wäre. Die ganze Sache hat aber noch einen weiteren großen Haken. Wie Sie vielleicht schon mitbekommen haben, ist es zwar wieder einfach möglich, Produkte zu modifizieren, aber das Hinzufügen neuer Produkte gestaltet sich als sehr aufwändig, da man alle Fabriken überarbeiten muss.

Abschließend sei noch erwähnt, dass es oft üblich ist, dass man das Fabrikmethodenmuster und die abstrakten Fabriken, gemeinsam nutzt. So werden oft Fabriken gebaut, die andere konkrete Fabriken erzeugen, welche dann erst ein konkretes Produkt erzeugen. In dem Sinne ist eine konkrete Fabrik also auch eine Art Produkt und die abstrakte Fabrik, also die Schnittstelle, wäre dann ein abstraktes Produkt. Sie sehen, dieses Spiel kann man sehr weit treiben und wenn man dann zusätzlich alles noch als Singleton macht, ist man an dem Punkt, an welchen man es auch übertreiben kann. Verwenden Sie also nicht auf Teufel komm raus Entwurfsmuster, sondern suchen Sie sich nur das günstigste heraus.

Zum Seitenanfang
Zum Inhaltsverzeichnis

33.2 Factory Method

33.2 Factory Method

Dieses Erzeugungsmuster dient zur Kapselung der Erzeugung und Implementierung von Produkten. Man schafft eine zentrale Stelle, an der Produkte erzeugt werden und wenn später neue Produkte hinzukommen, braucht man nur noch eine Stelle im Quelltext ändern und nicht unzählige. Dies reduziert Fehlerquellen.

Die zentrale Stelle wird durch eine Fabrik repräsentiert, welche nichts anderes macht, als ein gewünschtes Produkt zu erzeugen bzw. freizugeben. Wie der Produktwunsch der Fabrik mitgeteilt wird, bleibt dem Programmierer überlassen. Man könnte also ein String mit einem Klassennamen übergeben oder sich in der Fabrik einen Aufzählungstyp definieren.

Vereinfachte Struktur eines Factory Musters

Wie Sie sehen können, inkludiert der Client nur noch die Fabrik und kennt dadurch nur eine abstrakte Definition eines Produktes. Solange alle Produkte sich an diese Schnittstelle halten, braucht der Client sie nicht zu kennen, um sie zu benutzen. Dies macht vor allem dann Sinn, wenn man den Quelltext der Produkte nicht ausliefern möchte.

Ein typisches Anwendungsbeispiel wäre z.B. ein Computerspiel, welches mehrere Autos benutzen soll. Das Auto stellt eine abstrakte Schnittstelle dar, welche definiert, was ein Auto alles zu können haben muss. Ein Mercedes oder ein Opel wären dann konkrete Autos, welche durch die Fabrik erzeugt werden. Die Spiellogik selbst braucht nicht zu wissen, wie man ein Mercedes oder Opel erzeugen muss. Ihr ist es nur wichtig, dass sie die rudimentären Methoden aufrufen kann. Ein netter Nebeneffekt ist, dass die Implementierung eines konkreten Autos im Nachhinein aktualisiert werden kann, ohne das komplette Spiel neu kompilieren zu müssen. Einzige Bedingung ist, dass die Schnittstelle unberührt bleibt.

Im folgenden Quelltexten habe ich mich genau an dieses Beispiel mit den Autos gehalten und zeige Ihnen, wie man das Factory Methoden Pattern implementiert. Dabei habe ich wieder auf das ganze Drumherum verzichtet, damit der Fokus auf das Entwurfsmuster erhalten bleibt. Schauen Sie sich zunächst die Schnittstelle eines Autos an.

Auto.h:

 1
 2
 3
 4
 5
 6
					
// Schnittstelle für ein Auto
class IAuto {
	public:
		virtual void Anfahren()	= 0;
		virtual void Bremsen()	= 0;
};
					

Wie Sie sehen, wird ein Auto in meinem Beispiel nicht viel können, aber mit der abstrakten Definition der Methoden, erzwinge ich eine Implementierung dieser Methoden in den erbenden Klassen. Auffällig sollte hier der Klassenname sein. Ich leite ihn bewusst mit einem "I" und nicht mit "C" ein, um noch mehr herauszustellen, dass es sich um eine Schnittstelle (Interface), also um eine "pure abstract" Klasse handelt.

Es folgt die Definition der zwei konkreten Autos, welche dieses Interface implementieren.

Mercedes.h:

 1
 2
 3
 4
 5
 6
 7
 8
					
#include "Auto.h"

// Klasse für die Modelierung eines Mercedes
class CMercedes : public IAuto {
	public:
		virtual void Anfahren();
		virtual void Bremsen();
};
					

Opel.h:

 1
 2
 3
 4
 5
 6
 7
 8
					
#include "Auto.h"

// Klasse für die Modelierung eines Opels
class COpel : public IAuto {
	public:
		virtual void Anfahren();
		virtual void Bremsen();
};
					

Wie zu erwarten, passiert hier nichts spannendes und auch die folgende Implementierung wird nicht sehr viel aufregender.

Mercedes.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
					
#include <stdio.h>
#include "Mercedes.h"



// Implementierung der Beschleunigungsfunktion ////////////////////////////////
void CMercedes::Anfahren(void) {
	printf("Der Mercedes gibt Gas!\n");
} // Anfahren /////////////////////////////////////////////////////////////////



// Implementierung der Bremsfunktion //////////////////////////////////////////
void CMercedes::Bremsen(void) {
	printf("Der Mercesdes bremst\n");
} // Bremsen //////////////////////////////////////////////////////////////////
					

Opel.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
					
#include <stdio.h>
#include "Opel.h"



// Implementierung der Beschleunigungsfunktion ////////////////////////////////
void COpel::Anfahren(void) {
	printf("Der Opel gibt Gas!\n");
} // Anfahren /////////////////////////////////////////////////////////////////



// Implementierung der Bremsfunktion //////////////////////////////////////////
void COpel::Bremsen(void) {
	printf("Der Opel bremst\n");
} // Bremsen //////////////////////////////////////////////////////////////////
					

Wie Sie sehen, passiert tatsächlich nicht viel. Die zwei Methoden werden lediglich etwas auf der Konsole ausgeben, wenn sie aufgerufen werden. So kann ich Ihnen später zeigen, dass die Fabriken tatsächlich das tun, was sie sollen und weil wir dabei sind, hier die Definition und die Implementierung der Fabrik.

AutoFabrik.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
					
#include "Auto.h"

// Klasse zur Erzeugung eines Autos
class CAutoFabrik {
	public:
		enum EAuto {eaMercedes, eaOpel, eaCount, eaNone};
		static IAuto* CreateInstance(const EAuto& eAuto);
		static void ReleaseInstance(const EAuto& eAuto, IAuto*& pAuto);
};
					

AutoFabrik.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
					
#include "AutoFabrik.h"
#include "Mercedes.h"
#include "Opel.h"



// Erzeugt ein spezielles Auto ////////////////////////////////////////////////
IAuto* CAutoFabrik::CreateInstance(const EAuto& eAuto) {
	IAuto* pResult	= NULL;

	// Je nachdem, welches Auto erzeugt werden soll
	switch (eAuto) {
		case eaMercedes:	pResult = new Cmercedes();	break;
		case eaOpel:		pResult = new Copel();		break;
		// ...
	} // end of switch

	return pResult;
} // CreateInstance ///////////////////////////////////////////////////////////



// Freigeben eines speziellen Fahrzeuges //////////////////////////////////////
void CAutoFabrik::ReleaseInstance(const EAuto& eAuto, IAuto*& pAuto) {
	// Je nachdem, welches Auto erzeugt werden soll
	switch (eAuto) {
		case eaMercedes:	delete static_cast<CMercedes*>(pAuto);	break;
		case eaOpel:		delete static_cast<COpel*>(pAuto);	break;
		// ...
	} // end of switch

	pAuto = NULL;
} // ReleaseInstance //////////////////////////////////////////////////////////
					

Wie Sie anhand der Header-Datei sehen können, habe ich mich für einen Aufzählungstyp entschieden, um der Fabrik mitzuteilen, welches Produkt ich haben möchte. In der Implementierung der statischen Klassenmethoden, entscheide ich dann mittels einer Switch Anweisung, welches konkrete Auto erzeugt und somit zurückgegeben bzw. gelöscht wird. Dass man hier statische Klassenmethoden nutzt, ist auch sehr Typisch. Man braucht nicht extra ein Objekt, wenn man die Klasse nur nutzen will, um ein Objekt zu Erzeugen bzw. Freizugeben.

Ein großer Pferdefuß in C++ ist, dass man die Fabrik benötigt, um das Objekt wieder Freizugeben. Würde der Client das Objekt löschen, würde nur die Hälfte des Speichers freigegeben werden. Man muss sich also im Client irgendwie merken, was sich hinter einem Zeiger verbirgt. Ein Array aus Fahrzeugen ist also nicht unbedingt empfehlenswert. In solch einem Fall benötigt man ein Array aus Strukturen, in welcher man sich den Zeiger und den Typ hält. Wenn man allerdings das Singelton Pattern benutzt und jedes Auto nur ein einziges mal benötigt, geht man anders an die Sache heran. In solch einem Fall erzeugt man sich tatsächlich eine Instanz der Fabrik und dann ist auch die Methode zum Holen des Zeigers nicht mehr statisch. Intern könnte die Fabrik dann im Konstruktor ein Array mit allen möglichen Autos erzeugen und im Destruktor wieder freigeben. Entsprechend würde man die Methode zum Holen auch umbenennen in "GetInstance". Jene gibt dann nur den jeweiligen Zeiger aus dem internen Array zurück. Zudem deutet der Methodenname auch wieder auf das Singelton Pattern hin.

Interessant ist hierbei auch, dass nicht die Header-Datei der Fabrik die Produkte, also die konkreten Autos, includiert, sondern die CPP-Datei. Dies ist deshalb wichtig, damit man die Produkte besser ändern kann. Beispielweise ist es so möglich, dass ein spezielles Auto eine Eigenschaft mehr bekommt, ohne das ganze Spiel neu kompilieren zu müssen.

Abschließend noch die "main", welche den Client darstellt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
					
#include "AutoFabrik.h"



// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main (int argc, char** argv) {
	IAuto* pMercedes	= CAutoFabrik::CreateInstance(CAutoFabrik::eaMercedes);
	IAuto* pOpel		= CAutoFabrik::CreateInstance(CAutoFabrik::eaOpel);

	pMercedes->Anfahren();
	pOpel->Bremsen();

	CAutoFabrik::ReleaseInstance(CAutoFabrik::eaMercedes, pMercedes);
	CAutoFabrik::ReleaseInstance(CAutoFabrik::eaOpel, pOpel);

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

Der Mercedes gibt Gas!
Der Opel bremst!
		

Wie Sie anhand der Ausgabe sehen, arbeitet die Fabrik wie gewünscht und Sie finden in der "main" keinen Code mehr, welcher auf die speziellen Autos eingeht. Wichtig ist auch hier nochmal, dass das Erzeugen und Freigeben ausgelagert wurde und somit ebenfalls nicht mehr auftaucht.

Die ganze Sache hat aber einen großen Haken. Laut Definition ist die Schnittstelle der Produkte rein abstrakt, was zu Folge hat, dass man üblicherweise keine Eigenschaften definiert. Falls man dies macht, muss man sie mindestens "protected" definieren und auf Setter und Getter sollte man auch verzichten. Würde man sie implementieren, müsste man sie mindestens virtuell definieren, was wiederum sehr teuer ist, da wieder die Indirektionen über die virtuelle Tabelle, in den abgeleiteten Klassen hinzukommt.

Zum Seitenanfang
Zum Inhaltsverzeichnis

33.4 Builder

33.4 Builder

Die Erbauer ähneln sehr den abstrakten Fabriken, aber sie erzeugen keine Produktklasse, sondern eher ein Endresultat. Dies könnte z.B. eine Datei - oder Bildschirmausgabe sein. Zudem nennt man den Client hier oft Direktor oder führt den Direktor als Zwischenstück ein.

Wofür braucht man jetzt Builder? Sie sind vorwiegend für komplexe Strukturen bzw. Abläufe gedacht. Wärend eine Fabrik z.B. einen einfachen Schuh produziert, ist ein Builder eher damit beschäftigt ein Haus mit Garage, Balkon, Garten und Swimmingpool zu erzeugen, also durchaus ein Produkt mit mehreren Komponenten. Zudem kann es eine Rolle spielen, in welcher Reihenfolge die Komponenten erzeugt und miteinander verknüpft werden. Dabei wird der Außenwelt, also dem Direktor oder dem Client, nicht gezeigt, wie genau dieser Vorgang abäuft. Es wird also weniger das Produkt, als seine Erzeugung verschleiert bzw. Gekapselt.

Vereinfachte Struktur des Builder Pattern

Ein gutes Beispiel wäre z.B. ein Renderer. Er kümmert sich darum, das OpenGL oder DirectX spezifische Sachen vorbereitet werden, dann greift er auf alle Models zu, zeichnet sie entsprechend und führt dann ggf. spezielle Shader aus, damit die ganze Sache auch hübsch aussieht. Im Vorhergehenden Beispiel hatte jedes Produkt seine eigene Ausgabefunktion. Das Produkt wäre also die Bildschirmausgabe und nicht ein spezielles Model. Diese herangehensweise würde das vorhergehende Beispiel mit den abstrakten Fabriken, deutlich performanter machen.

In dem folgenden Beispiel werde ich dieses Entwurfsmuster, zum Speichern von Informationen, in eine Datei, benutzen. Mein Client wird also nicht nur für jede Dateiart eine eigene Methode benutzen, sondern zwei konkrete Erbauer, welcher das Speichern kapseln. Die Schnittstelle, also der abstrakte Builder, wird nur eine Methode zum Aufrufen der Speichernfunktion vorgeben. Die abgeleiteten speziellen Builder implementieren diese Methode und rufen von dort aus eigene private Methoden auf, welche dann Schritt für Schritt die notwendigen Aufgaben erledigen.

Schauen Sie sich zunächst die Schnittstelle an.

AbstractBuilder.h:

 1
 2
 3
 4
 5
					
// Schnittstelle für einen Dateierzeuger
class IAbstractFileBuilder {
	public:
		virtual void SaveToFile(const char* pcstrStream) = 0;
};
					

Wie Sie sehen, wird außer der Speichermethode, nichts weiter definiert. Als nächstes folgen die Definitionen der konkreten Builder.

PDFBuilder.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
					
#include "AbstractFileBuilder.h"

// Klasse zum Erzeugen von PDF Dateien
class CPDFBuilder : public IAbstractFileBuilder {
	public:
		CPDFBuilder(const char* pcstrFilePath);
		~CPDFBuilder();

		virtual void SaveToFile(const char* pcstrStream);

	private:
		char* m_pstrFilePath;

		void Open();
		void WriteMetaInformations();
		void WriteContent(const char* pcstrStream);
		void Close();
};
					

WordFileBuilder.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
					
#include "AbstractFileBuilder.h"

// Klasse zum Erzeugen von Word Dateien
class CWordFileBuilder : public IAbstractFileBuilder {
	public:
		CWordFileBuilder(const char* pcstrFilePath);
		~CwordFileBuilder();

		virtual void SaveToFile(const char* pcstrStream);

	private:
		char* m_pstrFilePath;

		void Open();
		void WriteHeader();
		void WriteContent(const char* pcstrStream);
		void WriteFooter();
		void Close();
};
					

Wie Sie sehen, unterscheiden sich beide Builder in der Anzahl der Methoden, aber es gibt auch Gemeinsamkeiten, wie den Dateiname und die Methoden zum Öffnen und Schließen. Jetzt könnte man meinen, dass man diese doch schon in der Basisklasse definieren kann. Hierauf muss ich mit einem klaren jain antworten.

Der Dateiname hat in einer Schnittstelle schon einmal nichts zu suchen. Der abstrakte Builder wäre dann zwar immer noch abstrakt, aber nicht mehr vollständig und dass sieht das Pattern nicht vor. Die Methoden wiederum könnten durchaus in die Basisklasse wandern, aber rein perspektivisch und semantisch, wäre das nicht korreckt. Im abstrakten Erbauer sollen nur die Methoden hinein, welche zum einen nichts über die Funktionsweise verraten (was das Öffnen und Schließen tun) und zweitens kann man garnicht davon ausgehen, dass man diese Funktionalitäten wirklich braucht. Man könnte sich genauso ein Builder erzeugen, welcher einfach auf die Konsole etwas ausgibt und diese Konsolenausgabe in eine Textdatei umleitet. Schon braucht man sich um das Öffnen und Schließen nicht mehr kümmern.

Als nächstes folgen die Implementierungen der Builder und wie Sie wieder feststellen werden, habe ich mich auf das wesentliche konzentriert und die eigentliche Arbeit durch simple Konsolenausgaben ersetzt.

PDFBuilder.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
					
#include "PDFBuilder.h"



// Konstruktor - Initialisieren ///////////////////////////////////////////////
CPDFBuilder::CPDFBuilder(const char* pcstrFilePath) {
	m_pstrFilePath = new char[strlen(pcstrFilePath) + 1];
	strcpy(m_pstrFilePath, pcstrFilePath);
} // CPDFBuilder //////////////////////////////////////////////////////////////



// Destruktor - Aufräumen /////////////////////////////////////////////////////
CPDFBuilder::~CPDFBuilder() {
	delete [] m_pstrFilePath;
} // ~CPDFBuilder /////////////////////////////////////////////////////////////



// Übergebenen String in PDF speichern ////////////////////////////////////////
void CPDFBuilder::SaveToFile(const char* pcstrStream) {
	Open();
	WriteMetaInformations();
	WriteContent(pcstrStream);
	Close();
} // SaveToFile ///////////////////////////////////////////////////////////////



// PDF-Datei öffnen ///////////////////////////////////////////////////////////
void CPDFBuilder::Open() { 
	printf("Open PDF-File\n");
} // Open /////////////////////////////////////////////////////////////////////



// Header-Informationen speichern /////////////////////////////////////////////
void CPDFBuilder::WriteMetaInformations() {
	printf("Write Word-Header\n");
} // WriteMetaInformations ////////////////////////////////////////////////////



// Hauptteil schreiben ////////////////////////////////////////////////////////
void CPDFBuilder::WriteContent(const char* pcstrStream) {
	printf("Write PDF-Content: %s\n", pcstrStream);
} // WriteContent /////////////////////////////////////////////////////////////



// Word-Datei schließen ///////////////////////////////////////////////////////
void CPDFBuilder::Close() { 
	printf("Close PDF-File\n");
} // Close ////////////////////////////////////////////////////////////////////
					

WordFileBuilder.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
					
#include "WordFileBuilder.h"



// Konstruktor - Initialisieren ///////////////////////////////////////////////
CWordFileBuilder::CWordFileBuilder(const char* pcstrFilePath) {
	m_pstrFilePath = new char[strlen(pcstrFilePath) + 1];
	strcpy(m_pstrFilePath, pcstrFilePath);
} // CWordFileBuilder /////////////////////////////////////////////////////////



// Destruktor - Aufräumen /////////////////////////////////////////////////////
CWordFileBuilder::~CWordFileBuilder() {
	delete [] m_pstrFilePath;
} // ~CWordFileBuilder ////////////////////////////////////////////////////////



// Übergebenen String in Word-Dokument speichern //////////////////////////////
void CWordFileBuilder::SaveToFile(const char* pcstrStream) {
	Open();
	WriteHeader();
	WriteContent(pcstrStream);
	WriteFooter();
	Close();
} // SaveToFile ///////////////////////////////////////////////////////////////



// Word-Datei öffnen //////////////////////////////////////////////////////////
void CWordFileBuilder::Open() { 
	printf("Open Word-File\n");
} // Open /////////////////////////////////////////////////////////////////////



// Header-Informationen speichern /////////////////////////////////////////////
void CWordFileBuilder::WriteHeader() {
	printf("Write Word-Header\n");
} // WriteHeader //////////////////////////////////////////////////////////////



// Hauptteil schreiben ////////////////////////////////////////////////////////
void CWordFileBuilder::WriteContent(const char* pcstrStream) {
	printf("Write Word-Content: %s\n", pcstrStream);
} // WriteContent /////////////////////////////////////////////////////////////



// Abschließende-Informationen speichern //////////////////////////////////////
void CWordFileBuilder::WriteFooter() {
	printf("Write Word-Footer\n");
} // WriteFooter //////////////////////////////////////////////////////////////



// Word-Datei schließen ///////////////////////////////////////////////////////
void CWordFileBuilder::Close() {
	printf("Close Word-File\n");
} // Close ////////////////////////////////////////////////////////////////////
					

Wie Sie sehen, erfüllen diese Klassen nicht das was sie versprechen, aber in diesem Kapitel geht es ja nicht darum, Worddokumente und PDF's zu erzeugt, sondern um Entwurfsmuster. Somit habe ich auch hier wieder alles weggelassen, was vom Fokus ablenken könnte.

Zu guter Letzt noch die Verwendung im Client, also im Direktor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
					
#include "PDFBuilder.h"
#include "WordFileBuilder.h"



// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main (int argc, char** argv) {
	CWordFileBuilder	oWordBuilder("C:\\Test.docx");
	CPDFBuilder		oPDFBuilder("C:\\Test.pdf");

	oWordBuilder.SaveToFile("Hallo Welt");
	printf("\n");
	oPDFBuilder.SaveToFile("Hallo Welt");

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

Open Word-File
Write Word-Header
Write Word-Content: Hallo Welt
Write Word-Footer
Close Word-File

Open PDF-File
Write Word-Header
Write PDF-Content: Hallo Welt
Close PDF-File
		

Wie Sie sehen, ruft der Direktor wirklich nur die Hauptfunktionalität auf und die speziellen Implementierungen sorgen für den Rest.

Dieses Entwurfsmuster wird allerdings weniger häufig genutzt, da die Methapher selten vorkommt, bzw. Anwendung findet und auch ein Austauschen nicht ganz so einfach ist bzw. das Kompilieren des gesamten Direktors mit sich zieht. Der vollständigkeit halber habe ich es aber mit aufgenommen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

35 Verhaltensmuster

35 Verhaltensmuster

coming soon...

Zum Seitenanfang
Zum Inhaltsverzeichnis


34 Strukturelle Muster

34 Strukturelle Muster

In diesem Kapitel wird es darum gehen, welche effektiven und erprobten Möglichkeiten es gibt, seine Software zu strukturieren. Die Erzeugung von Produkten spielt hier weniger eine Rolle, obwohl sie für eine adäquate Implementierung von Nöten ist.

Zum Seitenanfang
Zum Inhaltsverzeichnis

34.2 Composite

34.2 Composite

Das Kompositum beschreibt eine Struktur, in welcher man versucht, alle Objekte eines Frameworks in eine Baumstruktur zu fassen. Dabei ist aber nicht gemeint, dass man eine riesige Vererbungshierarchie schafft. Viel mehr ist damit gemeint, dass man eine Art Container implementiert, der alle anderen Objekte und oder sich selbst aufnehmen kann. Diese Herangehensweise wird bei fast jeder grafischen Bibliothek für Benutzeroberflächen eingesetzt.

Vereinfachte Struktur des Composite Pattern

Man muss sich das wie ein Dateisystem auf einer Festplatt vorstellen. Die "Component" stellt dabei ein beliebiger Eintrag im Dateisystem dar. Des weiteren gibt es verschiedenste Dateiarten (jede Dateiart stellt ein spezielles "Control" dar) und Verzeichnisse, also ein "Composite". Verzeichnisse können dann wieder Dateien und Verzeichnisse enthalten.

Wie Sie anhand der Grafik sehen können, stellt das Kompositum klassische Containermethoden bereit, welche es erlauben, Komponenten zu verwalten, also Hinzufügen, Entfernen, Aktualisieren usw. Mehr Fähigkeiten und Eigenschaften sollte es nicht haben.

Was sind jetzt aber die Folgen dieses Entwurfsmusters? Zunächst müssen alle Klassen von einer gemeinsamen Basisklasse erben. Das hat zur Folge, dass man auf Kurz oder Lang nicht um virtuelle Methoden herumkommt, was wiederum ungünstig für die Ausführungsgeschwindigkeit ist. Des weiteren handelt es sich bei einem Kompositum um einen ganz allgemeinen Container. Aufgrund von Polymorphismus sind wir dann zwar in der Lage, alle erstellten Objektarten aufzunehmen, ohne ein Template benutzen zu müssen, aber das hat auch zur Folge, dass man am Ende nicht mehr weiß, was genau aufgenommen wurde. Jede Komponente benötigt also spezielle Informationen über sich selbst (z.B. den tatsächlichen Klassennamen als String).

Auf eine Implementierung verzichte ich an dieser Stelle, da ich zum einen schon gezeigt habe, wie man einen Container implementiert und zum anderen eher von diesem Pattern abrate, da man sich mehr Probleme als Vorteile einkauft.

Zum Seitenanfang
Zum Inhaltsverzeichnis

34.3 Bridge

34.3 Bridge

Das Entwurfsmuster der Brücken ist eine weitere Art, wie man mehrere Klassen in Verbindung miteinander bringen kann. Zudem schafft man es, eine Schnittstelle von seiner Implementierung zu lösen. Das hat den Vorteil, dass die Schnittstelle und ihre Implementierung unabhängig voneinander Modifiziert und oder Erweitert werden können. Eine Erweiterung der Schnittstelle hat also nicht zur Folge, dass man etwas in der Implementierung anpassen muss.

Aber wie schafft man es jetzt, eine Schnittstelle und die Implementierung zu entkoppeln? Der Trick ist eine etwas andere Denkweise. Statt die Implementierung von der Schnittstelle erben zu lassen, aggregiert die Schnittstelle in diesem Entwurfsmuster seine Implementierung.

Vereinfachte Struktur des Bridge Pattern

Wenn Sie sich die Abbildung anschauen, erkennen Sie, dass die Schnittstelle eigentlich kein Interface mehr ist (zu mindestens nicht im klassischen Sinne). Aber wozu benötigt man jetzt dieses Muster? Schauen Sie sich dazu folgende Grafik an.

Beispiel für eine exponentielle Vererbungshierarchie

Ich gebe zu, dass dieses Beispiel etwas an den Haaren herbeigezogen ist, aber man kann an ihm sehr gut die Problematik und deren Lösung zeigen. Wie Sie sehen, habe ich eine Schnittstelle zur allgemeinen Definition einer Datei und eines Verzeichnisses. Nun werden diese aber je nach Dateisystem unterschiedlich abgebildet und implementiert. Deshalb gibt es jetzt für jede Dateisystemart, je eine erbende Klasse für Dateien und Verzeichnisse. Wenn also irgendwann eine neue Dateisystemart unterstützt werden soll, benötigt man zwei neue Klassen, obwohl nur ein System hinzu kommt. Noch problematischer wird es, wenn ein neues Dateisystemobjekt hinzu kommt, wie z.B. Verknüpfungen (Softlinks). In diesem Fall müsste man genauso viele neue Klassen schreiben, wie es Dateisysteme gibt. Sie merken also, dass diese Struktur sehr schnell sehr groß werden kann und ich habe absichtlich ein harmloses Beispiel gewählt.

Dieses unangenehme Verhalten kann man jetzt mit Brücken lösen. Man baut diese Klassenstruktur jetzt so um, dass zwei Bäume entstehen, nämlich einen mit Dateisystemobjekten (die Schnittstelle) und eine mit Dateisystemarten (die Implementierung der Dateisystemobjekte).

Auf das Bridge Pattern angepasstes Beispiel

Jede Datei und jedes Verzeichnis bekommt also sein Dateisystem aggregiert und kann sich somit über seine spezielle Implementierung verwalten. Wie Sie jetzt auch einsehen werden, benötigt man für die Implementierung eines neuen Dateisystemobjektes nur noch eine neue Klasse und für die Implementierung eines neuen Dateisystems, benötigt man ebenfalls nur noch eine neue Klasse. Wenn Sie jetzt auch noch bedenken, dass eine Datei und das Dateisystem, in welchem sie gehalten wird, eigentlich unmittelbar miteinander verbunden sind, verstehen Sie auch, was ich damit meinte, dass man eine Schnittstelle (also beispielsweise wie ein Datei - oder Verzeichnis aussehen darf) und die zugehörige Implementierung (wie genau wird jetzt beispielsweise die Datei unter FAT oder NTFS gespeichert und verwaltet) voneinander trennen kann.

Hier also mal auszugsweise der Quelltext zu diesem Beispiel. Ich fange mit der Dateisystemimplementierung an.

FileSystemImplementation.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
					
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Interface für eine Dateisystemimplementierung
class CFileSystemImplementation {
	public:
		virtual void	SaveStream(char* pstrPath, char* pstrStream) = 0;
		virtual char*	LoadStream(char* pstrPath) = 0;
};
					

Wie Sie sehen, ist diese Schnittstelle sehr rudimentär. Auf ein komplexes Beispiel kommt es mir aber nicht an. Wichtig ist das Prinzip. Als nächstes folgt eine konkrete Umsetzung eines Dateisystemes, welches diese Interface implementiert.

FAT.h:

 1
 2
 3
 4
 5
 6
 7
 8
					
#include "FileSystemImplementation.h"

// Klasse zur Implementierung des FAT Dateisystemes
class CFAT : public CFileSystemImplementation {
	public:
		virtual void	SaveStream(char* pstrPath, char* pstrStream);
		virtual char*	LoadStream(char* pstrPath);
};
					

FAT.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
					
#include "FAT.h"



// Speichern unter FAT ////////////////////////////////////////////////////////
void CFAT::SaveStream(char* pstrPath, char* pstrStream) {
	if (pstrStream != NULL) {
		printf("Speichere '%s' in Datei '%s'\n", pstrStream, pstrPath);
	} // end of if
} // SaveStream ///////////////////////////////////////////////////////////////



// Laden unter FAT ////////////////////////////////////////////////////////////
char* CFAT::LoadStream(char* pstrPath) {
	char* pstrTemp = new char[11];

	strcpy(pstrTemp, "Hallo Welt");
	printf("Lese '%s' aus Datei '%s'\n", pstrTemp, pstrPath);

	return pstrTemp;
} // LoadStream ///////////////////////////////////////////////////////////////
					

Ok, das war wie zu erwarten nicht spannend, zumal es auch fraglich ist, ob man auf diese Art und Weiße auch eine Verknüpfung speichern kann, aber das spielt ja jetzt keine Rolle. Richtig interessant wird es erst jetzt, wenn ich die Schnittstelle für Dateisystemobjekt entwerfe, welche diese Implementierungen nutzen soll.

FileSystemObject.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
					
#include "FileSystemImplementation.h"

// Schnittstelle für ein Objekt eines Dateisystemes
class IFileSystemObject {
	public:
		IFileSystemObject(CFileSystemImplementation* pImplementation) : m_pBridge(pImplementation) {}

		virtual char* Load() = 0;
		virtual void Save(char* pstrStream) = 0;

	protected:
		CFileSystemImplementation* m_pBridge;
};
					

FileClass.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
					
#include "FileSystemObject.h"

// Basisklasse für eine Datei
class CFileClass : public IFileSystemObject {
	public:
		CFileClass(CFileSystemImplementation* pImplementation, char* pstrFileName);
		~CFileClass(void);

		virtual char* Load(void);
		virtual void Save(char* pstrStream);

	private:
		char* m_pstrFileName;
};
					

FileClass.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
					
#include "FileClass.h"



// Konstruktur - Initialisieren ///////////////////////////////////////////////
CFileClass::CFileClass(CFileSystemImplementation* pImplementation, char* pstrFileName)
	: IFileSystemObject(pImplementation)
	, m_pstrFileName(NULL)
{
	// Nur wenn ein Dateiname übergeben wurde
	if (pstrFileName != NULL) {
		int iLength	= strlen(pstrFileName) + 1;
		m_pstrFileName	= new char[iLength];
		strcpy(m_pstrFileName, pstrFileName);
	} // end of if
} // CFileClass ///////////////////////////////////////////////////////////////



// Destruktor - Speicher freigeben ////////////////////////////////////////////
CFileClass::~CFileClass(void) {
	// Prüfen, ob Speicher freigegeben werden muss
	if (m_pstrFileName != NULL)	delete [] m_pstrFileName;
} // ~CFileClass //////////////////////////////////////////////////////////////



// Inhalt laden ///////////////////////////////////////////////////////////////
char* CFileClass::Load() {
	// Nur wenn eine Implementierung vorliegt
	if (m_pBridge != NULL && m_pstrFileName != NULL) {
		return m_pBridge->LoadStream(m_pstrFileName);
	} else {
		return NULL;
	} // end of if
} // Load /////////////////////////////////////////////////////////////////////



// Inhalt speichern ///////////////////////////////////////////////////////////
void CFileClass::Save(char* pstrStream) {
	// Nur wenn eine Implementierung vorliegt
	if (m_pBridge != NULL && m_pstrFileName != NULL) {
		m_pBridge->SaveStream(m_pstrFileName, pstrStream);
	} // end of if
} // Save /////////////////////////////////////////////////////////////////////
					

Nun, auch das war jetzt letztendlich nicht so spannend. Flüchtig betrachtet zeigt dieses Beispiel eher, wie man mit aggregierten Objekten umgeht und das ist auch nicht verwunderlich, da dies ja das Grundkonzept des Brückenentwurfsmusters ist. Der interessante Part ist ein wenig im Quelltext versteckt.

Wichtig ist hier eigentlich die Aufgabenverteilung. Eine Datei an sich, sollte normalerweise Informationen über ihren Namen besitzen und gleichzeitig ihren Inhalt verwalten können (letzteres habe ich weggelassen). Das Dateisystem kümmert sich dann um das Speichern der selben und kann prüfen, ob der Dateiname gültig ist. Ob der Dateiname aber überhaupt gesetzt ist, sollte wiederum die Datei selber prüfen. Hier weicht dieses Entwurfsmuster also von der mir gewählten Metapher ab (ein tatsächliches Dateisystem übernimmt normalleweise alle diese Aufgaben und die Datei ist üblicherweise ein reiner Datencontainer). Des weiteren könnte man meinen, dass dieses Pattern dem Adapter ähnelt und in gewissen Hinsicht ist dem auch so, aber bei genauem Betrachten gibt es Unterschiede, welche genau in der Aufgabenverteilung liegen. Ein Adapter gibt Aufrufe lediglich weiter und sollte keine eigene Logik implementieren. Die Brücke hingegen sieht eine klare Aufgabenverteilung vor.

Abschließend noch die Implementierung im Client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
					
#include "FileClass.h"
#include "FAT.h"



// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main (int argc, char** argv) {
	CFAT*		pImplementation	= new CFAT();
	CFileClass	oFile(pImplementation, "Test.txt");

	printf("Rufe LOAD auf:\n");
	char*		pstrTemp		= oFile.Load();

	printf("%s\n\n", pstrTemp);
	printf("Rufe SAVE auf:\n");
	oFile.Save(pstrTemp);

	delete [] pstrTemp;
	delete pImplementation;

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

Rufe LOAD auf:
Lese ' Hallo Welt' aus Datei 'Test.txt'
Hallo Welt

Rufe SAVE auf:
Speichere 'Hallo Welt' in Datei 'Test.txt'
		
Zum Seitenanfang
Zum Inhaltsverzeichnis

34.5 Flyweight

34.5 Flyweight

Das Entwurfsmuster der Fliegengewichte ist etwas schwerer zu erklären, da es kein Grundlegenden Bauplan gibt, sondern von Fall zu Fall anders implementiert wird. Die Grundidee ist, dass man so viele Attribute wie möglich auslagert. Somit bricht es mit dem klassischen objektorientierten Ansatz. Während eine Klasse normalerweise so aufgebaut wird, dass sie weitestgehend autark ist (sie bringt alle Attribute und Methoden mit um sich selbst zu verwalten), werden Fliegengewichte so konzipiert, dass sie nur die Eigenschaften beinhalten, welche sie einzigartig machen. Alle berechenbaren Zustände werden in den Container verlagert, welche sie beinhaltet.

Warum bricht man also mit diesem "alt bewährtem" Konzept? Man entscheidet sich nur dann für dieses Entwurfsmuster, wenn abzusehen ist, dass man Unmengen dieser Objekte benötigt und somit würde jedes Attribut, welches auch berechenbar ist, unnötig Speicher verschwenden. Ein häufiges Anwendungsgebiet ist die 3D Programmierung. Hier gibt es oft tausende bis Millionen von Objekten/Polygonen. Würde man diese in eine große Objekthierarchie zwingen und am besten noch virtuelle Methoden überschreiben, blähen sich solche Objekte schnell auf und irgendwann besitzt man nicht mehr genügend Grafikspeicher, um sie darzustellen. Man beschränkt sie also auf ein Minimum. Beispielsweise werden Methoden zur Darstellung entfernt. Es wird dann eher einen zentralen Renderer geben, welcher sich um das Zeichnen kümmert. Das Ganze geht sogar soweit, dass selbst Positionswerte der Verteces, nicht mehr in den Objekten gehalten wird. Sie besitzen dann lediglich einen Zeiger auf eine große Liste, in welcher alle Vektoren gehalten werden. Selbst Farben und oder Texturen werden ausgelagert und man findet in der Klasse der Körper nur noch Verweise.

Im ersten Moment denk man, dass man damit ein Programm langsamer macht und für spezielle Fälle trifft das auch zu, aber wenn man sich die Programmstruktur im Ganzen betrachtet erkennt man, dass man doch einiges einsparen kann. Zum Beispiel wird das Transformieren eines bestimmten Objektes länger dauern, da es für den Zugriff auf seine Matrix, eine Indirektion benötigt. Möchte man jetzt aber alle Objekte zeichnen, sieht die Sache ganz anders aus. Wenn man beliebig viele Objekte verwalten will, benötigt man definitiv Zeiger auf diese Objekte. Ein Renderer der also tausend Polygone zeichnen will, müsste also tausend Indirektionen vornehmen, um an die Matrizen zu gelangen. Verwaltet man aber diese Matrizen in einer zentralen Liste, benötigt man nur die eine Indirektion auf jene Liste. Hinzu kommt, dass man jetzt viel schonender mit dem Speicher arbeitet, also wesentlich freundlicher mit dem Cache umgeht. Statt also unnötige Informationen wie Name usw. mit in den Cache zu laden, werden nur noch reine Vektoren geholt.

Ein anderer Anwendungsfall wäre z.B. ein Schachspiel. Das Schachbrett besteht aus 64 Feldern. Verwaltet man jene in einer Liste bzw. in einem zweidimensionalen Array, benötigen die Feldklassen keine Angabe mehr über ihre Position, weil sich diese aus der Position in der Liste bzw. im Array, ergeben. Die Schachfigur selbst benötigt auch keine Information mehr über ihre Position, wenn ein Zeiger auf das entsprechende Feld gesetzt wird. So reduziert man also zwei Angaben auf eine.

Sie sehen also, dass es in gewissen Situationen klüger sein kann, sich vom klassischen objektorientierten Ansatz zu lösen, wenn man schonendere Herangehensweisen in Betracht ziehen kann und dies mit bedacht macht. Die ganze Sache hat aber einen Hakten. Indem man immer mehr Informationen aus - bzw. umlagert, wird man immer unflexibler und kann die erstellten Klassen immer weniger wiederverwenden. Dieser Kompromiss wird aber gerade im Computerspielsektor oft eingegangen, zumal man eh die meisten Sachen nicht wiederverwendet (sie unterliegen einfach einem zu großen technischem Wandel, dass man sie eh meist von Grund auf neu konzipieren muss).

Zum Seitenanfang
Zum Inhaltsverzeichnis

34.1 Adapter und Facade

34.1 Adapter und Facade

Diese zwei Entwurfsmuster dienen der Kapselung von Schnittstellen und kommen immer dann zum Einsatz, wenn man eine vorgefertigte fremde Schnittstelle hat, welche man nicht so ansprechen will oder kann, wie es einem vorgegeben wird. Im Volksmunde ist hier oft die Rede von Wrappern, also Hüllen oder Verpackungen. Wie der Name schon suggeriert, mappt man mit diesen Klassen eigene Aufrufe auf ganz andere. Die eigene Anwendung stellt hier wieder den Client dar, während die Schnittstelle bzw. die fremde Bibliothek der Dienst ist, der angesprochen werden soll.

Stellen Sie sich vor, sie wollten tatsächlich ein Spiel programmieren und dabei DirectrX benutzen. Nun haben Sie sich aber für die Datenhaltung ganz andere Datenstrukturen konstruiert. DirectX verlangt aber ständig eigene Datentypen. An dieser Stelle haben Sie zwei Möglichkeiten. Entweder Sie passen Ihre gesamte Anwendung an und nehmen immer vor den Aufrufen entsprechende Umwandlungen vor oder Sie bauen einen Adapter, welcher Ihre Objekte in andere umwandelt und die gewünschte Methode aufruft. Letztere Variante hat den Vorteil, dass diese Umwandlungen nicht so tief in Ihrer Applikation verwurzelt sind und somit leichter auf Änderungen von DirectX reagiert werden kann. Zudem gibt es eine zentrale Stelle für die Umwandlungen.

Nun unterteilt man die Adapter in zwei verschiedene Kategorien, nämlich die Objektadapter und die Klassenadapter.

Vereinfachte Struktur des Adapter Pattern

Objektadapter Implementieren ein Zielinterface und komponieren den Dienst. Intern wird dann der Zugriff auf das Zielinterface so implementiert, dass das Interface des Dienstes benutzt wird. Klassenadapter sind in vielen Programmiersprachen nicht möglich, wohl aber in C++, da diese Sprache Mehrfachvererbung erlaubt. Der Unterschied ist, dass der Klassenadapter nicht die eigentliche Schnittstelle preisgibt (da privat geerbt wird) und auch nicht alle Methoden implementieren muss.

Adapter benutzt man um s.g. Hüllenklassen zu entwerfen, welche sich wiederum in zwei Untergruppen teilen lassen. Das eine mal verpackt man Primitivtypen und das andere mal verpackt man Bibliotheken bzw. mehrere Funktionen, welche nicht durch eine Klasse gekapselt werden. In diesem Fall spricht man von einer Facade.

Vereinfachte Struktur des Facade Pattern

Ein Verwendungszweck für eine Hüllenklasse wäre die Klasse "CString", welche den Primitivtyp "char" verpackt. So könnte sich der String gleich intern merken wie groß bzw. lang er ist und wenn diese Klasse dann noch zusätzliche Operatoren für die Addition oder für Vergleiche bereitstellt, wird die Verwendung von Strings in C++ wesentlich angenehmer. Ich habe an dieser Stelle auf ein Beispiel mit Quelltext verzichtet, da man eine solche Klasse in den Standardbibliotheken findet. Auch in Java sind solche Hüllenklassen üblich. Beispielsweiße wird der Primitivtyp "int", durch die Klasse "Integer" gekapselt.

Ein Beispiel für eine Facade wäre meine Threadwrapperklasse, welche ich Ihnen bereits vorgestellt hatte. Hier kapsle ich die Funktionen der WinAPI und bringe sie in einen gemeinsamen Kontext, um das Arbeiten mit Threads zu erleichtern bzw. ergonomischer zu gestalten.

Zum Seitenanfang
Zum Inhaltsverzeichnis

34.6 Proxy

34.6 Proxy

Der Begriff Proxy sollte Ihnen aus dem Bereich der Netzwerktechnik bekannt vorkommen. Sie sind Stellvertreter für andere Dinge und stellen somit eine Art Adapter dar. Im Gegensatz zum Entwurfsmuster der Adapter, bilden Proxys die Schnittstelle eines Subjektes ab und erweitern sie ggf. um weitere Funktionalitäten.

Vereinfachte Struktur des Proxy Pattern

Wozu wird dieses Entwurfsmuster benötigt? Proxys werden in verschiedenen Bereichen eingesetzt und so unterscheidet man vier verschiedene Arten, je nach Anwendungsgebiet.

Die erste Art, die "Remoteproxys", kommen immer dann zum Einsatz, wenn man auf ein Objekt zugreifen will, welches lokal oder im Netzwerk liegt. Dabei soll es nach außen hin keinen sichtbaren Unterschied geben. Im Sektor der COM Programmierung (DCOM und ActiveX), gibt es immer solche Stellvertreterobjekte.

Dann gibt es s.g. "virtuelle Proxys". Sie kapseln meist größere Objekte und werden häufig in Verbindung mit den Fliegengewichten und Singleton eingesetzt. Die teuren Objekte werden meist nur einmalig angelegt und die möglichen Abweichungen werden auf die Proxys verlagert, welche dann für die eigentliche Arbeit, alle auf die gleiche Objektinstanz verweisen.

Die dritte Art der Proxys kommt immer dann zum Einsatz, wenn man verschiedene Zugriffsrechte auf ein Objekt kapseln möchte. Man nennt diese Stellvertreter "Schutzproxys". Ein einfaches Beispiel wäre der Zugriff auf ein Konto. Während das Konto nur Methoden zum ändern bereitstellt, wird eine Passwortabfrage im Proxy realisiert. Ein etwas komplexeres Beispiel könnte diverse Benutzerrechte kapseln. So bräuchte das eigentliche Objekt nicht ständig prüfen, welcher Benutzer gerade eine Anfrage stellt und ob er das nötige Recht besitzt, sondern der Benutzer bekommt einen ihm zugewiesenen Proxy, welcher dann nur eine Teilmenge der Funktionalitäten bereitstellt, also nicht das komplette Interface erbt oder implementiert (bei einer Vererbung müssen schon alle abstrakten Methoden implementiert werden, aber manche machen dann einfach nichts).

Letztlich gibt es da noch die s.g. "Smart References", welche den Zugriff und die Speicherverwaltung eines Objektes kapseln. Dabei wird jede Referenz auf das eigentliche Objekt mitgezählt und falls die letzte Referenz entfernt wurde, wird dafür gesorgt, dass das Objekt freigegeben wird (man baut sich somit einen Carbage Collector). Zudem ist es möglich, die eigentlichen Objekte mit Zusatzfunktionen auszustatten, was dem Dekorationsmuster gleich kommt. Das eigentliche Objekt und seine Schnittstelle bleiben davon unberührt.

Es gibt Situationen, in welchen es Sinnvoll ist, den Stellvertreter nicht von der Schnittstelle erben zu lassen. Im Falle der Schutzproxys ist dies beispielsweise Sinnvoll (da man sonst, wie erwähnt, Methoden bereitstellen müsste, welche keine Funktionalität besitzen).cIm folgenden Beispiel werde ich Ihnen zeigen, wie ein solcher Schutzproxy aussehen könnte. Es wird darum gehen, eine Klasse zu entwerfen, welche auf eine Datenbank zugreift. Zudem entwerfe ich zwei Proxys, welche unterschiedliche Zugriffsrechte realisieren (einer nur mit Leserecht und der andere mit vollem Zugriffsrecht). Um die Sache möglichst flexibel zu halten, wird es noch zwei Basisklassen geben, wobei eine, eine Allgemeine Datenbankverbindung beschreibt und die andere, einen allgemeinen Proxy. Schauen Sie sich zunächst das Klassendiagramm an.

Möglicher Aufbau einer gesicherten Datenbankverbindung

Wie Sie sehen, halte ich die Schnittstelle und somit die Funktionsweise, sehr einfach. Neben dem Verbinden bzw. Trennen der Verbindung, wird es nur eine Methode zum Lesen und eine zum Schreiben geben, wobei ich letztere absichtlich mit keinen Parametern ausgestattet habe, da es mir weniger auf das tatsächliche Lesen und Schreiben ankommt. Diese zwei Methoden werden später lediglich etwas auf der Konsole ausgeben.

Fangen wir mit der Definition der Schnittstelle für Datenbankverbindungsklassen an.

AbstractDBConnection.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
					
#include <stdlib.h>

// Schnittstelle für ein Datenbankobjekt
class CAbstractDBConnection {
	public:
		// Datenbankverbindung aufbauen
		virtual void Connect()		= 0;
		// Datenbankverbindung trennen
		virtual void Disconnect()	= 0;
		// Daten auslesen
		virtual void GetData() const	= 0;
		// Daten speichern
		virtual void SetDate() const	= 0;

	protected:
		static CAbstractDBConnection*	s_pConnection;
		static bool			s_bConnected;

		// Konstruktor
		CAbstractDBConnection();
};
					

Was an dieser Stelle besonders auffällig sein sollte ist, dass der Konstruktor geschützt ist. Damit verhindere ich, dass man sich später nach belieben eine Instanz der Klasse erzeugen kann, da sie als Singleton benutzt werden soll. Zu diesem Zweck habe ich auch eine statische Klassenvariable definiert. Abgeleitete Klassen müssen also noch die Methoden "GetInstance" und "Release" implementieren. Warum habe ich die Methode und die Klassenvariablen jetzt als geschützt und nicht als privat deklariert? Dies ist ganz einfach zu erklären. Wären sie privat, könnten abgeleitete Klassen nicht mehr auf den Basiskonstruktor zugreifen und die Methoden zum Erzeugen und Freigeben hätten keinen Zugriff mehr auf die Klassenvariablen. Auf private Methoden und Eigenschaften kann man in abgeleiteten Klassen nicht mehr zugreifen, jedoch auf öffentliche und geschützte Bereiche.

Als nächstes folgt die Implementierung dieser Klasse.

AbstractDBConnection.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
					
#include "AbstractDBConnection.h"



CAbstractDBConnection*	CAbstractDBConnection::s_pConnection	= NULL;
bool			CAbstractDBConnection::s_bConnected	= false;

// Konstruktor
CAbstractDBConnection::CAbstractDBConnection() {}
					

Wie zu erwarten war, erfolgt hier lediglich die Initialisierung der Klassenvariablen und die formale Implementierung des Konstruktors. In meiner Implementierung hat der Konstruktor keine Aufgabe, was aber in einer tatsächlichen Implementierung nicht der Fall sein muss. Auch hier habe ich wieder eine Vereinfachung vorgenommen.

Als nächstes folgt eine konkrete Datenbankverbindung auf Basis von MySQL, wobei ich auch hier aus Gründen der Vereinfachung, auf spezifische Merkmale verzichtet habe und somit nur der Klassenname auf diese Datenbankart hinweist.

MySQLConnection.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
					
#include <stdio.h>
#include "AbstractDBConnection.h"

// Klasse zur Kommunikation mit einer MySQL Datenbank
class CMySQLConnection : public CAbstractDBConnection {
	public:
		// Ggf. Datenbankverbindung aufbauen
		virtual void Connect();
		// Ggf. Datenbankverbindung trennen
		virtual void Disconnect();
		// Daten aus der Datenbank holen
		virtual void GetData() const;
		// Daten in die Datenbank schreiben
		virtual void SetDate() const;

		// Ggf. Objektinstanz erzeugen
		static CMySQLConnection* GetInstance();
		// Ggf. Objektinstanz freigeben
		static void Release();

	private:
		// Konstruktor - Initialisieren
		CMySQLConnection();
};
					

Neben der Überladung der abstrakten Methoden, kommen hier noch die zwei Methoden zum Erzeugen und Freigeben der Instanz hinzu, wie Sie dies vom Singleten Pattern gewohnt sind. Auch der Konstruktor ist jetzt privat, was zur Folge hat, dass von dieser Klasse nicht mehr geerbt werden kann.

Es folgt nun die Implementierung dieser Klasse.

MySQLConnection.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
					
#include "MySQLConnection.h"



// Konstruktor - Initialisieren ///////////////////////////////////////////////
CMySQLConnection::CMySQLConnection() : CAbstractDBConnection() {} /////////////



// Ggf. Datenbankverbindung aufbauen //////////////////////////////////////////
void CMySQLConnection::Connect() {
	// Nur wenn noch keine Verbindung besteht
	if (!s_bConnected) {
		s_bConnected = true;
		printf("CMySQLConnection::Connect\n");
	} // end of if
} // Connect //////////////////////////////////////////////////////////////////



// Ggf. Datenbankverbindung trennen ///////////////////////////////////////////
void CMySQLConnection::Disconnect() {
	// Nur wenn eine Verbindung besteht
	if (s_bConnected) {
		s_bConnected = false;
		printf("CMySQLConnection::Disconnect\n");
	} // end of if
} // Disconnect ///////////////////////////////////////////////////////////////



// Daten aus der Datenbank holen //////////////////////////////////////////////
void CMySQLConnection::GetData() const {
	// Nur wenn eine Verbindung besteht
	if (s_bConnected) printf("CMySQLConnection::GetData\n");
} // GetData //////////////////////////////////////////////////////////////////



// Daten in die Datenbank schreiben ///////////////////////////////////////////
void CMySQLConnection::SetDate() const {
	// Nur wenn eine Verbindung besteht
	if (s_bConnected) printf("CMySQLConnection::SetData\n");
} // SetDate //////////////////////////////////////////////////////////////////



// Ggf. Objektinstanz erzeugen ////////////////////////////////////////////////
CMySQLConnection* CMySQLConnection::GetInstance() {
	// Instanze erzeugen, wenn sie noch nicht existiert
	if (s_pConnection == NULL) s_pConnection = new CMySQLConnection();

	return static_cast<CMySQLConnection*>(s_pConnection);
} // GetInstance //////////////////////////////////////////////////////////////



// Ggf. Objektinstanz freigeben ///////////////////////////////////////////////
void CMySQLConnection::Release() {
	// Instanz freigeben, wenn es sie gibt
	if (s_pConnection != NULL) {
		// Wenn noch eine Datenbankverbindung besteht
		if (s_bConnected) s_pConnection->Disconnect();

		delete s_pConnection;
		s_pConnection = NULL;
	} // end of if
} // Release //////////////////////////////////////////////////////////////////
					

Wie bereits angekündigt, passiert hier nicht wirklich viel und somit ist die Implementierung mehr oder weniger formaler Natur. Einziges Augenmerk liegt hier auf den Methoden "GetInstance" und "Release". Nach dem Erzeugen muss man noch eine Typumwandlung vornehmen, da die statische Klassenvariable in der Basisklasse definiert wurde und man somit nur einen Zeiger auf die Schnittstelle und nicht auf die eigentliche Klassen besäße (Polymorphismus). Beim Freigeben muss zudem noch sichergestellt werden, dass die Verbindung ordnungsgemäß beendet wird, falls dies noch nicht erledigt wurde.

Im nächsten Teil wird es schon interessanter, denn jetzt folgen die Proxys, welche die eben definierte Datenbankverbindung umhüllt. Da hier nicht mehr viel Logik vorhanden ist (die Aufrufe werden nur weitergeleitet), habe ich mir erlaubt, die Methoden inline zu definieren. Zudem habe ich mich für ein Template entschieden, damit ich die Proxys auf alle Datenbankarten anwenden kann. Ohne den Templatemechanismus müsste ich sonst für jede Datenbankverbindung, eigene Stellvertreter entferfen.

Auch hier wird es wieder eine Basisklasse geben, um die Implementierungen der konkreten Proxys so schlank wie möglich zu halten. Zudem vermeide ich das Definieren abstrakter Methoden, damit die abgeleiteten Stellvertreter, zum Einen schlank bleiben und zum Anderen keine virtuellen Tabellen benutzen müssen, was sich positiv auf die Ausführungsgeschwindigkeit auswirken wird. Gerade dieser Aspekt sollte nicht außer acht gelassen werden, da man sich durch Proxys ohnehin zusätzliche Funktionsaufrufe einhandelt.

AbstracktDBConnectionProxy.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
					
#include <stdio.h>

// Basisklasse für einen Datenbankverbindungsstellvertreter
template<class TDBConnectionType>
class CAbstractDBConnectionProxy abstract {
	public:
		// Konstruktor - Initialisieren
		CAbstractDBConnectionProxy() : m_pConnection(NULL) {
			m_pConnection = TDBConnectionType::GetInstance();
		} // CabstractDBConnectionProxy

		// Destructor - Aufräumen
		~CAbstractDBConnectionProxy() {
			m_pConnection = NULL;
			TDBConnectionType::Release();
		} // ~CabstractDBConnectionProxy

		// Datenbankverbindung aufbauen
		void Conncect() { m_pConnection->Connect(); }

		// Datenbankverbindung trennen
		void Disconnect() { m_pConnection->Disconnect(); }

	protected:
		CAbstractDBConnection* m_pConnection;
};
					

Wie Sie sehen, kümmert sich die Basisklasse um grundlegende Aufgaben, wie das Verwalten der Verbindung, da dies für jede Datenbankart notwendig ist. Da sich aus Sicht des Proxys dieses Verhalten nie ändert (wie die Datenbankverbindung tatsächlich aufgebaut werden muss, regelt die Datenbankverbindungsklasse), können diese Methoden also ohne Bedenken in der Basisklasse platziert werden.

Da jetzt das grundlegende Verhalten Implementiert wurde, können jetzt die speziellen Schutzproxys entworfen werden. Da die Basisklasse wie erwähnt den Großteil schon kapseln, werden sie sehr klein und deshalb habe ich die zwei konkreten Stellvertreter, in einer Datei untergebracht.

DBConnectionProxys:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
					
#include "AbstractDBConnectionProxy.h"

// Klasse, welche nur lesenden Zugriff auf eine Datenbank gestattet
template<class TDBConnectionType>
class CReadOnlyDBConnection : public CAbstractDBConnectionProxy<TDBConnectionType> {
	public:
		// Lesender Zugriff
		void GetData() const {
			m_pConnection->GetData();
		} // GetDate
};



// Klasse, welche lesenden und schreibenden Zugriff auf eine Datenbank gestattet
template<class TDBConnectionType>
class CFullAccessDBConnection : public CAbstractDBConnectionProxy<TDBConnectionType> {
	public:
		// Lesender Zugriff
		void GetData() const {
			m_pConnection->GetData();
		} // GetDate

		// Lesender Zugriff
		void SetData() const {
			m_pConnection->SetDate();
		} // GetDate
};
					

Sie sehen also, dass es jetzt ohne Probleme möglich ist, Teile einer Schnittstelle zu implementieren, um so den Client in seiner Handlungsmöglichkeit einzuschränken.

Des weiteren sollte auffallen, dass zumindest die Methode "GetData" doppelt implementiert wurde. Hier könnte man noch auf die Idee kommen, die Klasse mit dem Vollzugriff von der eingeschränkten Klasse erben zu lassen um diese Dopplung zu Vermeiden. Von diesem Vorgehen rate ich allerdings aus mehreren Gründen ab. Zum Einen sollte man nur Vererben, wenn sich im realen Leben Methaphern für die einzelnen Klassen bilden lassen. Basisklassen zu schaffen, nur weil etwas doppelt erscheint, kann auf Kurz oder Lang, nach hinten losgehen, was mich zum zweiten Grund bringt. Auch das Lesen könnte unterschiedlich implementiert werden. Beispielsweise kann der eine Proxy auf Tabelle A und B zugreifen und ein anderer auf Tabelle A und C. Von welcher Klasse soll man jetzt erben? Klüger wäre es also, sich für jede Tabelle einen Proxy zu bauen und jene dann in einer zentralen Instanz zu aggregieren. Andernfalls kann sich die Vererbungshierarchie sehr schnell aufblähen und unübersichtlich werden. Zum Anderen sollten die Proxys, wie erwähnt, nicht viel Logik implementieren, was zur Folge hat, dass Aufrufe in den meisten Fällen nur weiter delegiert werden. Somit h¨lt sich eine mehrfache Implementierung einer Methode in Grenzen. Zudem ist die Wahrscheinlichkeit, dass sich an diesem Verhalten später etwas ändern wird, sehr gering (Dopplungen sind ja immer dann kritisch, wenn es Änderungen gibt und somit x Stellen überarbeitet werden müssen, da man Gefahr läuft, einige zu vergessen).

Abschließend folgt jetzt noch die beispielhafte Implementierung des Clients mit einer Testausgabe.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
					
#include "MySQLConnection.h"
#include "DBConnectionProxys.h"



// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main(int argc, char** argv) {
	CReadOnlyDBConnection<CMySQLConnection>	oReadOOnlyConnection;
	CFullAccessDBConnection<CMySQLConnection>	oFullConnection;

	oReadOOnlyConnection.Conncect();
	oFullConnection.Conncect();

	oReadOOnlyConnection.GetData();
	oFullConnection.SetData();

	oFullConnection.Disconnect();
	oReadOOnlyConnection.Disconnect();

	return 0;
} // main /////////////////////////////////////////////////////////////////////						
					

Ausgabe:

CMySQLConnection::Connect
CMySQLConnection::GetData
CMySQLConnection::SetData
CMySQLConnection::Disconnect
		
Zum Seitenanfang
Zum Inhaltsverzeichnis

34.4 Decorator

34.4 Decorator

Das Entwurfsmuster Decorator, ist eine weitere Möglichkeit, Komponenten um Fähigkeiten zu erweitern. Wieder versucht man eine große Vererbungshierarchie zu entschärfen, indem man versucht eine Komponente und seine Fähigkeiten zu trennen. Man muss sich das in etwa wie ein Plugin vorstellen.

Vereinfachte Struktur des Decorator Pattern

Auf den ersten Blick sieht diese Geschichte wie das Composite Pattern aus, aber die Denkweise ist genau die entgegengesetzte. Während man im Kompositum mehr auf interne Fähigkeiten eingeht, beschäftigen sich die Dekorationen mit äußeren Fähigkeiten. Zum Beispiel kann ein Fenster verschiedene Komponenten beinhalten. Es zeichnet sich also selbst und ruft anschließend die Zeichenfunktionen der enthaltenen Kinder auf. Das Fenster steht also an oberster Stelle. Wie gesagt, funktionieren die Dekorationen genau umgekehrt. So könnte ein Fenster mit verschiedenen Rahmen dekoriert werden. In diesem Fall wird also erst der Dekorator anfangen den Rahmen zu zeichnen und dann die beinhaltete Komponente zeichnen lassen. In diesem Fall steht das Fenster also ganz unten bzw. am Ende der Verarbeitungskette.

Hier mal ein Beispiel, um dies besser zu verdeutlichen. Stellen Sie sich folgende ungünstige Vererbungshierarchie vor.

Beispiel für eine verschachtelte Vererbungshierarchie

Abgesehen davon, dass Mehrfachvererbung sehr Ungünstig ist, schränkt einen diese Struktur ein und macht es schwer, neue Fähigkeiten hinzuzufügen. Wenn man jetzt das Decorator Pattern zur Anwendung bringt, würde die Resultierende Vererbungshierarchie wie folgt aussehen.

Nach dem Decorator Pattern umstrukturiertes Beispiel

Wie Sie sehen, ist dies schon wesentlich übersichtlicher und zudem einfacher erweiterbar. Ein weiterer günstiger Nebeneffekt dieser Struktur ist, dass man die Dekorationen, also die Fähigkeiten, zur Laufzeit austauschen kann, was die vorhergehende Struktur nicht erlaubt. Aber wie bekommt man jetzt wieder ein Fenster hin, welches modal ist, Fadeeffekte besitzt und zudem ein Gradient im Hintergrund aufweist? Schauen Sie sich dazu folgende Grafik an

Mögliche Implementierung des Beispieles

Wie Sie sehen, können Dekorationen wiederum Dekorationen aufnehmen (und so weiter), bis letztlich die eigentliche Komponente folgt. Um dies noch mehr zu verdeutlichen, zeige ich Ihnen eine mögliche Implementierung dieses Beispieles, wobei ich es leicht abwandeln werde, um zu zeigen, dass man die Komponenten tatsächlich zur Laufzeit austauschen kann. Ich beginne mit der Definition der Schnittstelle.

WindowInterface.h:

 1
 2
 3
 4
 5
 6
 7
 8
					
#include <stdlib.h>
#include <stdio.h>

// Schnittstelle für alle Komponenten
class IWindow {
	public:
		virtual void Draw(void) = 0;
};
					

Wie sie sehen können, wird mein Fenster nicht viel können, außer sich auszugeben. Ich habe wieder bewusst auf alle Spielereien verzichtet, damit der Fokus auf dem Entwurfsmuster erhalten bleibt. Es folgt nun die Definition und Implementierung einer konkreten Fensterklasse.

WindowImplementation.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
					
#include "WindowInterface.h"

// Klasse zur Darstellung eines Fensters
class CWindowImplementation : public IWindow {
	public:
		CWindowImplementation(int& iValue) : m_iValue(iValue) {}

		virtual void Draw(void);

	private:
		int m_iValue;
};
					

WindowImplementation.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
					
#include "WindowImplementation.h"



// Fenster Zeichnen ///////////////////////////////////////////////////////////
void CWindowImplementation::Draw(void) {
	printf("CWindowImplementation(%i)", m_iValue);
} // Draw /////////////////////////////////////////////////////////////////////
					

Diese Klasse besitzt lediglich ein Attribut, welches in der Zeichenfunktion mit dem Klassennamen ausgegeben wird. Der Fokus liegt hier auf der Ausgabe. Das Attribut wird bewusst innerhalb der Klammern ausgegeben, um zu verdeutlichen, dass interne Member auch intern gezeichnet werden.

Es folgt nun die Definition der Dekorationen und ihrer Schnittstelle. Aus Gründen der Vereinfachung, habe ich sie alle in einer Header-Datei zusammengefasst. Für eine tatsächliche Realisierung rate ich aber davon ab.

Decorator.h:

 1
 2
 3
 4
 5
 6
 7
 8
19
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
					
#include "WindowImplementation.h"

// Basisklasse für eine Dekoration
class CDecorator : public IWindow {
	public:
		CDecorator(IWindow* pComponent) : m_pComponent(pComponent) {}

		virtual void Draw(void) = 0;

	protected:
		IWindow* m_pComponent;
};



// Klasse zur Gradientendekoration einer Komponente
class CGradientEffect : public CDecorator {
	public:
		CGradientEffect(IWindow* pComponent) : CDecorator(pComponent) {}

		virtual void Draw(void);
};


// Klasse zur Fadedekoration einer Komponente
class CFadeEffect : public CDecorator {
	public:
		CFadeEffect(IWindow* pComponent) : CDecorator(pComponent) {}

		virtual void Draw(void);
};



// Klasse zur Modaldekoration einer Komponente
class CModalEffect : public CDecorator {
	public:
		CModalEffect(IWindow* pComponent) : CDecorator(pComponent) {}

		virtual void Draw(void);
};
					

Decorator.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
					
#include "Decorator.h"



// Dekoration und Komponente Zeichnen /////////////////////////////////////////
void CGradientEffect::Draw(void) {
	// Wenn es eine Komponente gibt
	if (m_pComponent != NULL) {
		printf("CGradientEffect(");
		m_pComponent->Draw();
		printf(")");
	} // end of if
} // Draw /////////////////////////////////////////////////////////////////////



// Dekoration und Komponente Zeichnen /////////////////////////////////////////
void CFadeEffect::Draw(void) {
	// Wenn es eine Komponente gibt
	if (m_pComponent != NULL) {
		printf("CFadeEffect(");
		m_pComponent->Draw();
		printf(")");
	} // end of if
} // Draw /////////////////////////////////////////////////////////////////////



// Dekoration und Komponente Zeichnen /////////////////////////////////////////
void CModalEffect::Draw(void) {
	// Wenn es eine Komponente gibt
	if (m_pComponent != NULL) {
		printf("CModalEffect(");
		m_pComponent->Draw();
		printf(")");
	} // end of if
} // Draw /////////////////////////////////////////////////////////////////////
					

Wie Sie anhand der Zeichenfunktionen sehen können, wird erst die Dekoration selbst gezeichnet und anschließend erfolgt der Aufruf der Zeichenfunktion der Komponente. Das hier nach diesem Aufruf noch etwas gezeichnet wird, habe ich in diesem Fall nur aus ästhetischen Gründen gemacht.

Abschließend noch die Implementierung in der "main".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
					
#include "Decorator.h"
#include "WindowImplementation.h"



// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main(int argc, char** argv) {
	int			iValue		= 1;
	CWindowImplementation*	pWindow		= new CWindowImplementation(iValue);
	CGradientEffect*	pGradient	= new CGradientEffect(pWindow);
	CFadeEffect*		pFade		= new CFadeEffect(pGradient);

	pFade->Draw();
	printf("\n");

	delete pFade;

	CModalEffect*		pModal		= new CModalEffect(pGradient);

	pModal->Draw();
	printf("\n");

	delete pWindow;
	delete pGradient;
	delete pModal;

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

CFadeEffect(CGradientEffect(CWindowImplementation(1)))
CModalEffect(CGradientEffect(CWindowImplementation(1)))
		

Wie die Ausgabe zeigt, ist es also in der Tat möglich, die Dekorationen während der Laufzeit umzuhängen. Momentan ist das Austauschen der Dekorationen noch etwas unergonomisch, aber wenn man noch zusätzliche Getter und Setter implementiert, kann man auch ohne weiteres die Dekorationen in der Mitte austauschen. Des weiteren sieht man sehr schön, dass sich die Dekorationen um die eigentliche Komponente, also das Fenster, hüllen.

Abschließend möchte ich aber noch auf ein großen Nachteil eingehen. Da die Decorator Eigenschaften der Wrapper aufweisen, welche auch u.a. die Aufrufe weiterleiten, nachdem sie ihre eigene Logik abgearbeitet haben, müssen sie auch die komplette Schnittstelle implementieren. Eine Erweiterung der Komponenten hat also zur Folge, dass auch alle Dekorationen erweitert werden müssen, obwohl die Neuerung vielleicht gar nichts mit der jeweiligen Dekoration zu tun hat.

Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012