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...

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

© Copyright by Thomas Weiß, 2009 - 2012