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

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

© Copyright by Thomas Weiß, 2009 - 2012