Erbauer (Entwurfsmuster)
Der Erbauer (englisch builder) ist ein Entwurfsmuster aus dem Bereich der Softwareentwicklung. Es gehört zur Kategorie der Erzeugungsmuster (englisch creational patterns) und trennt die Konstruktion komplexer Objekte von deren Repräsentationen, wodurch dieselben Konstruktionsprozesse wiederverwendet werden können.[1] Das Muster ist eines der sogenannten GoF-Muster (Gang of Four, siehe Viererbande).
Verwendung
Der Einsatz des Erbauer-Entwurfsmusters bietet sich an, wenn
- zu einem komplexen Objekt unterschiedliche Repräsentationen existieren sollen,
- die Konstruktion eines komplexen Objekts unabhängig von der Erzeugung der Bestandteile sein soll oder
- der Konstruktionsablauf einen internen Zustand erfordert, der vor einem Klienten verborgen werden soll.
Typische Anwendungen sind z. B. Anwendungsprogramme zur Konvertierung.
Akteure
Man kann vier Akteure unterscheiden: Direktor
, Erbauer
, KonkreterErbauer
und Produkt
. Der Erbauer spezifiziert eine abstrakte Schnittstelle zur Erzeugung der Teile eines komplexen Objektes. Der konkrete Erbauer erzeugt die Teile des komplexen Objekts durch Implementierung der Schnittstelle. Außerdem definiert und verwaltet er die von ihm erzeugte Repräsentation des Produkts. Er bietet auch eine Schnittstelle zum Auslesen des Produkts.
Der Direktor konstruiert ein komplexes Objekt unter Verwendung der Schnittstelle des Erbauers. Der Direktor arbeitet eng mit dem Erbauer zusammen: Er weiß, welche Baureihenfolge der Erbauer verträgt oder benötigt. Der Direktor entkoppelt somit den Konstruktionsablauf vom Klienten. Das Produkt repräsentiert das zu konstruierende komplexe Objekt.
Vorteile
Die Implementierungen der Konstruktion und der Repräsentationen werden isoliert. Die Erbauer verstecken ihre interne Repräsentation vor dem Direktor. Neue Repräsentationen lassen sich leicht durch neue konkrete Erbauerklassen einfügen. Der Konstruktionsprozess wird an einer dedizierten Stelle (im Direktor) gesteuert; spätere Änderungen – etwa ein Mehrphasen-Konstruktionsprozess statt einer Einphasen-Konstruktion – lassen sich ohne Änderung der Klienten realisieren.
Nachteile
Es besteht eine enge Kopplung zwischen Produkt, konkretem Erbauer und den am Konstruktionsprozess beteiligten Klassen.[2]
Variante
Man kann auch das Produkt selber die Erbauer-Schnittstelle implementieren lassen. Dadurch erspart man sich u. U. einige Klassen. Das erzeugte Produkt „schleppt“ die Erbauer-Schnittstelle sein ganzes Leben mit sich herum, sodass auch später von außen Produktteile angebaut werden können.
Verwendung in der Analyse
Dieses Muster wird in der Software-Analyse wegen der schwierigen Metapher selten verwendet.
Die Variante, bei der ein Objekt selbst Verfahren zur Verfügung stellt, um weitere Teile anzubauen, ist vorteilhaft in Pipeline-artigen Geschäftsprozessen. Der Geschäftsprozess als Direktor weist das Dokument als Erbauer an, neue Teile zu erzeugen und in sich einzuhängen. Beispielsweise kann eine Aktenverwaltung in einzelnen Schritten Vermerke an einen Aktenlauf anhängen.
Beispiel
Diese C++11 Implementierung basiert auf der vor C++98 Implementierung im Buch Entwurfsmuster.
#include <iostream>
enum Richtung {Norden, Sueden, Osten, Westen};
class KartenEintrag {
public:
virtual void betrete() = 0;
virtual ~KartenEintrag() = default;
};
class Raum : public KartenEintrag {
public:
Raum() :raumNr(0) {}
Raum(int n) :raumNr(n) {}
void setSeite(Richtung d, KartenEintrag* ms) {
std::cout << "Raum::setSeite " << d << ' ' << ms << '\n';
}
virtual void betrete() {}
Raum(const Raum&) = delete; // Dreierregel
Raum& operator=(const Raum&) = delete;
private:
int raumNr;
};
class Wand : public KartenEintrag {
public:
Wand() {}
virtual void betrete() {}
};
class Tuer : public KartenEintrag {
public:
Tuer(Raum* r1 = nullptr, Raum* r2 = nullptr)
:raum1(r1), raum2(r2) {}
virtual void betrete() {}
Tuer(const Tuer&) = delete; // Dreierregel
Tuer& operator=(const Tuer&) = delete;
private:
Raum* raum1;
Raum* raum2;
};
class Labyrinth {
public:
void fuegeRaumHinzu(Raum* r) {
std::cout << "Labyrinth::fuegeRaumHinzu " << r << '\n';
}
Raum* raumNr(int) const {
return nullptr;
}
};
class LabyrinthErbauer {
public:
virtual ~LabyrinthErbauer() = default;
virtual void baueLabyrinth() = 0;
virtual void baueRaum(int raum) = 0;
virtual void baueTuer(int roomFrom, int roomTo) = 0;
virtual Labyrinth* getLabyrinth() {
return nullptr;
}
protected:
LabyrinthErbauer() = default;
};
// Wenn baueLabyrinth ein Objekt erhält, das ein neues Labyrinth vollständig unter Verwendung von Operationen zum Hinzufügen von Räumen, Türen und Wänden zum Labyrinth selbst erzeugen kann, dann können Sie Vererbung benutzen, um Teile des Labyrinths oder die Art, wie es gebaut wird, zu verändern. Dies ist ein Beispiel für das Erbauermuster (119).
class LabyrinthSpiel {
public:
Labyrinth* baueLabyrinth(LabyrinthErbauer& erbauer) {
erbauer.baueLabyrinth();
erbauer.baueRaum(1);
erbauer.baueRaum(2);
erbauer.baueTuer(1, 2);
return erbauer.getLabyrinth();
}
Labyrinth* erzeugeComplexLabyrinth(LabyrinthErbauer& erbauer) {
erbauer.baueRaum(1);
// ...
erbauer.baueRaum(1001);
return erbauer.getLabyrinth();
}
};
class StandardLabyrinthErbauer : public LabyrinthErbauer {
public:
StandardLabyrinthErbauer() :aktuellesLabyrinth(nullptr) {}
virtual void baueLabyrinth() {
aktuellesLabyrinth = new Labyrinth;
}
virtual void baueRaum(int n) {
if (!aktuellesLabyrinth->raumNr(n)) {
Raum* raum = new Raum(n);
aktuellesLabyrinth->fuegeRaumHinzu(raum);
raum->setSeite(Norden, new Wand);
raum->setSeite(Sueden, new Wand);
raum->setSeite(Osten, new Wand);
raum->setSeite(Westen, new Wand);
}
}
virtual void baueTuer(int n1, int n2) {
Raum* r1 = aktuellesLabyrinth->raumNr(n1);
Raum* r2 = aktuellesLabyrinth->raumNr(n2);
Tuer* d = new Tuer(r1, r2);
r1->setSeite(gemeinsameWand(r1,r2), d);
r2->setSeite(gemeinsameWand(r2,r1), d);
}
virtual Labyrinth* getLabyrinth() {
return aktuellesLabyrinth;
}
StandardLabyrinthErbauer(const StandardLabyrinthErbauer&) = delete; // Dreierregel
StandardLabyrinthErbauer& operator=(const StandardLabyrinthErbauer&) = delete;
private:
Richtung gemeinsameWand(Raum*, Raum*) {
return Norden;
}
Labyrinth* aktuellesLabyrinth;
};
int main() {
LabyrinthSpiel spiel;
StandardLabyrinthErbauer erbauer;
spiel.baueLabyrinth(erbauer);
erbauer.getLabyrinth();
}
Die Programmausgabe ist ähnlich zu:
Labyrinth::fuegeRaumHinzu 0x23f3ed0
Raum::setSeite 0 0x23f4300
Raum::setSeite 1 0x23f4320
Raum::setSeite 2 0x23f4340
Raum::setSeite 3 0x23f4360
Labyrinth::fuegeRaumHinzu 0x23f4380
Raum::setSeite 0 0x23f43a0
Raum::setSeite 1 0x23f43c0
Raum::setSeite 2 0x23f43e0
Raum::setSeite 3 0x23f4400
Raum::setSeite 0 0x23f4420
Raum::setSeite 0 0x23f4420
Eine Börsensoftware hält Aktienkurse in einer Textdatei in folgendem Format fest: Pro Aktiengesellschaft werden in einer Zeile, durch Leerzeichen getrennt, Wertpapierkennnummer, Name der Aktiengesellschaft, Kurs und gehandelte Stückzahl gespeichert:
515100 BASF 36,84 2850400 803200 Commerzbank 6,71 17231300 ...
Nun soll dieses Format in ein Format wie CSV oder XML umgewandelt werden. Wird im CSV-Format das Semikolon als Trennzeichen benutzt, so soll obige Datei beispielsweise in folgende umgewandelt werden:
515100;BASF;36,84;2850400 803200;Commerzbank;6,71;17231300
Im XML-Format dagegen könnte das Ergebnis der Umwandlung so aussehen:
<Aktienkurse> <Aktie> <WKN>515100</WKN> <Name>BASF</Name> <Kurs>36,84</Kurs> <Stueckzahl>2850400</Stueckzahl> </Aktie> <Aktie> <WKN>803200</WKN> <Name>Commerzbank</Name> <Kurs>6,71</Kurs> <Stueckzahl>17231300</Stueckzahl> </Aktie> </Aktienkurse>
Das folgende C++-Programm zeigt den Einsatz des Erbauer-Musters in einer Applikation zur Datenformat-Umwandlung, die leicht um weitere Ausgabeformate erweiterbar ist. Der Direktor (Klasse KursdatenUmwandler
) weiß, wie Daten im Altformat einzulesen und zu parsen sind. Er kennt einen Erbauer, der geparste Teile in sein jeweiliges Format übersetzen kann. Alle konkreten Erbauer sind konkrete Unterklassen der abstrakten Klasse KursdatenBauer
. Beispielsweise übersetzt die Klasse XMLKursdatenBauer
geparste Zeilen in ein XML-Format.
Der Klient kann dem Direktor den konkreten Erbauer zur Laufzeit mitteilen. So kann das Ausgabeformat zur Laufzeit gewechselt werden.
Um ein neues Ausgabeformat zu unterstützen, muss nur die Klasse KursdatenBauer
entsprechend durch eine konkrete Unterklasse implementiert werden, z. B. durch LaTeXKursdatenBauer
.
Wichtig ist bei diesem Muster Folgendes: Es sind nicht nur die erzeugten Einzelteile, die Komplexität besitzen (darum kümmern sich die konkreten Erzeuger), sondern auch das zu erzeugende Ganze ist ein komplexes Objekt, um dessen Erzeugung sich der Direktor kümmert. Der Direktor ist also der „Fachmann“ für die Erzeugung des Produktes. Er allein kennt die notwendigen Einzelschritte. Im Beispiel weiß allein er, wie das Altformat zu parsen und daraus das neue Format zusammenzusetzen ist.
#include <iostream>
#include <memory>
#include <string>
using std::cin;
using std::cout;
using std::endl;
using std::shared_ptr;
using std::string;
// Abstrakter Erbauer:
class KursdatenBauer {
public:
virtual void KursdatenSchreiben(const string &wkn, const string &name,
const string &kurs,
const string &stueckzahl) = 0;
virtual void SchreibenInitialisieren() {}
virtual void SchreibenBeenden() {}
};
// Konkreter Erbauer für CSV-Dateien:
class CSVKursdatenBauer : public KursdatenBauer {
DatenRepraesentation &repr_;
public:
CSVKursdatenBauer(DatenRepraesentation &arepr) : repr_(arepr) {}
// Hier entsteht das Produkt, der Einfachheit halber auf die
// Standardausgabe geschrieben. (Es könnte auch in einen Stream
// geschrieben werden, der im Konstruktor übergeben wird.)
virtual void KursdatenSchreiben(const string &wkn, const string &name,
const string &kurs,
const string &stueckzahl) {
repr_.anhaengen(wkn)
.anhaengen(";")
.anhaengen(name)
.anhaengen(";")
.anhaengen(kurs)
.anhaengen(";")
.anhaengen(stueckzahl);
}
virtual void SchreibenInitialisieren() { repr_.loeschen(); }
};
// konkreter Erbauer für XML-Dateien:
class XMLKursdatenBauer : public KursdatenBauer {
DatenRepraesentation &repr_;
public:
XMLKursdatenBauer(DatenRepraesentation &arepr) : repr_(arepr) {}
virtual void KursdatenSchreiben(const string &wkn, const string &name,
const string &kurs,
const string &stueckzahl) {
repr_.anhaengen("\t<Aktie>").anhaengen("\n");
repr_.anhaengen("\t\t<WKN>").anhaengen(wkn).anhaengen("</WKN>\n");
repr_.anhaengen("\t\t<Name>").anhaengen(name).anhaengen("</Name>\n");
repr_.anhaengen("\t\t<Kurs>").anhaengen(kurs).anhaengen("</Kurs>\n");
repr_.anhaengen("\t\t<Stueckzahl>").anhaengen(stueckzahl).anhaengen("</Stueckzahl>\n");
repr_.anhaengen("\t</Aktie>").anhaengen("\n");
}
virtual void SchreibenInitialisieren() {
repr_.loeschen();
repr_.anhaengen("<Aktienkurse>").anhaengen("\n");
}
virtual void SchreibenBeenden() {
repr_.anhaengen("</Aktienkurse>").anhaengen("\n");
}
};
// Produkt
class DatenRepraesentation {
string text;
public:
DatenRepraesentation &anhaengen(const string &teil) {
text += teil;
return (*this);
}
void ausgeben() {
cout << text << endl;
}
void loeschen() {
text.clear();
}
};
// Direktor:
class KursdatenUmwandler {
shared_ptr<KursdatenBauer> _kursdatenBauer;
public:
void KursdatenBauerSetzen(shared_ptr<KursdatenBauer> kb) {
_kursdatenBauer = kb;
}
void KursdatenParsenUndSchreiben() {
_kursdatenBauer->SchreibenInitialisieren();
// Zeile für Zeile von STDIN lesen und in geeignete Teile zerlegen
while (!cin.eof()) {
string wkn, name, kurs, stueckzahl;
// lesen:
cin >> wkn >> name >> kurs >> stueckzahl;
if (wkn.empty()) {
break;
}
// schreiben:
_kursdatenBauer->KursdatenSchreiben(wkn, name, kurs, stueckzahl);
}
_kursdatenBauer->SchreibenBeenden();
}
};
// Klient:
int main() {
DatenRepraesentation repraesentation;
shared_ptr<KursdatenBauer> csvKursdatenBauer(new CSVKursdatenBauer(repraesentation));
shared_ptr<KursdatenBauer> xmlKursdatenBauer(new XMLKursdatenBauer(repraesentation));
KursdatenUmwandler kursdatenUmwandler;
kursdatenUmwandler.KursdatenBauerSetzen(xmlKursdatenBauer);
// oder
// kursdatenUmwandler.KursdatenBauerSetzen(csvKursdatenBauer);
kursdatenUmwandler.KursdatenParsenUndSchreiben();
// Aktion mit dem Produkt ausführen
repraesentation.ausgeben();
}
Verwandte Entwurfsmuster
Die abstrakte Fabrik ähnelt dem Erbauer, weil sie ebenfalls komplexe Objekte erzeugen kann. Dabei steht aber nicht die Struktur im Vordergrund, sondern die Abstraktion vom konkreten Typ der erzeugten Objekte. Der Erbauer erzeugt oft ein Kompositum (Entwurfsmuster). Bei Applikationen zur Konvertierung ist der Direktor – oder sogar der Erbauer – oft ein Besucher oder eventuell ein Interpreter (Entwurfsmuster) der Struktur, die konvertiert werden soll.
Weblinks
Einzelnachweise
- ↑ Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Entwurfsmuster. 5. Auflage. Addison-Wesley, 1996, ISBN 3-8273-1862-9, S. 119.
- ↑ Karl Eilebrecht, Gernot Starke: Patterns kompakt. Entwurfsmuster für effektive Software-Entwicklung. 3. Auflage. Spektrum Akademischer Verlag, 2010, ISBN 978-3-8274-2525-6, S. 29, doi:10.1007/978-3-8274-2526-3.