Unser Kunde möchte, dass wir ein Framework zur Erstellung von verschiedenen stereotypen Spielwelten (Regenwald, Wüste, Polargebiet) kreieren. Wir sollen eine API bereitstellen mit dem sich Objekte für die Spielwelt (Pflanzen, Tiere, Untergrund) generieren lassen. Unser Framework soll Methoden zur Generierung dieser Elemente zu Verfügung stellen.
Der Kunde möchte, dass wir ihm zu Beginn einen Prototyp erstellen: Ein Spielweltgenerator für die Spielwelt Regenwald. Für den Regenwald wählen wir Bäume, Elefanten und Gras als Spielweltelemente. Wir erstellen eine Factory, die den Erstellungscode kapselt und zentralisiert: der Regenwaldgenerator.

Code:
class Regenwaldgenerator{
public Elefant createElefant(){
return new Elefant();
}
public Baum createBaum(){
return new Baum();
}
public Gras createGras(){
return new Gras();
}
}
class Elefant{}
class Baum{}
class Gras{}
Über den Regenwaldgenerator (der Factory) kann der Client (also der Anwender unseres Spielweltgenerators) beliebige Elemente erstellen:
Über den Regenwaldgenerator erstellt der Client seine konkreten Spielweltelemente:
public static void main(String[] args) {
Regenwaldgenerator regenwaldgenerator = new Regenwaldgenerator();
Baum baum1 = regenwaldgenerator.createBaum();
Baum baum2 = regenwaldgenerator.createBaum();
Elefant elefant = regenwaldgenerator.createElefant();
Gras gras = regenwaldgenerator.createGras();
//Welt zusammen setzen...
}
Der Kunde kann nun fleißig Regenwaldwelten erstellen, ist begeistert und möchte nun auch andere Welten (Wüste und Polargebiet) generieren. Übermütig und unüberlegt wird folgender Entwurf aus der Hüfte geschossen:
Für jeden Spielwelttyp wird ein eigener Generator geschrieben. Der Client wählt je nach gewünschter Spielwelt den passenden Generator und holt sich die konkreten Spielweltelementen von diesen.
Unser Framework lässt sich sehr unkomfortabel nutzen und der Clientcode wird komplex:
public static void main(String[] args) {
//Flags die bestimmen, welchen Weltentyp der Client bauen soll
boolean regenwald = true;
boolean polargebiet = false;
boolean wüste = false;
//Elemente holen
if (regenwald){
Regenwaldgenerator regenwaldgenerator = new Regenwaldgenerator();
Baum baum1 = regenwaldgenerator.createBaum();
Baum baum2 = regenwaldgenerator.createBaum();
Elefant elefant = regenwaldgenerator.createElefant();
Gras gras = regenwaldgenerator.createGras();
//Welt zusammen setzten...
} else if (wüste){
Wüstengenerator wüstengenerator = new Wüstengenerator();
Kaktus kaktus1 = wüstengenerator.createKaktus();
Kaktus kaktus2 = wüstengenerator.createKaktus();
Kamel kamel = wüstengenerator.createKamel();
Sand sand = wüstengenerator.createSand();
//Welt zusammen setzten...
} else if (polargebiet){
Polargebietgenerator polargebietgenerator = new Polargebietgenerator();
// etc.
//Welt zusammen setzten...
}
}
Mit diesem Entwurf haben wir dem Unsinn Türen und Tore geöffnet:
Unser hohes Ziel ist es, dass der Client generischen Code zur Spielwelt schreiben kann ("Ich nehme 20 Pflanzen und 12 Tiere auf dem passenden Untergrund, ordne sie an und lasse sie interagieren."). Es soll keine Rolle spielen, von welchem konkreten Typ die Spielwelt (Regenwald, Wüste, Polargebiet) ist, die er gerade baut. Der Client soll dies bei der Spielweltgestaltung nicht einmal mehr wissen und sich auf das Zusammensetzen der Spielwelt konzentrieren.
Der Schlüssel zur dieser angestrebten Allgemeingültigkeit und zur Lösung all unserer Probleme liegt in Abstraktion. Erinnern wir uns an folgendes OO-Entwurfsprinzip:
Stütze dich auf Abstraktion, nie auf konkrete Klassen.
Wir führen Abstraktionen zu unseren Spielweltelementen in Form der Interfaces IPflanze, ITier und IUntergrund ein.

Auch führen wir eine Schnittstelle für unsere Generatoren ein. Dieses Interface (AbstractGenerator) definiert die drei createX()-Methoden, liefert aber nun nicht mehr konkrete Klassen zurück, sondern die eben definierten Spielweltobjektabstraktionen (IPflanze, ITier, IUntergrund) zurück.
Code des Interfaces AbstractGenerator und dessen Implementierungen Regenwald-, Wüsten und Polargebietgenerator:
interface AbstractGenerator{
public ITier createTier();
public IPflanze createPflanze();
public IUntergrund createUntergrund();
}
class Regenwaldgenerator implements AbstractGenerator{
public ITier createTier(){
return new Elefant();
}
public IPflanze createPflanze(){
return new Baum();
}
public IUntergrund createUntergrund(){
return new Gras();
}
}
class Wüstengenerator implements AbstractGenerator{
public ITier createTier(){
return new Kamel();
}
public IPflanze createPflanze(){
return new Kaktus();
}
public IUntergrund createUntergrund(){
return new Sand();
}
}
class Polargebietgenerator implements AbstractGenerator{
public ITier createTier(){
return new Eisbär();
}
public IPflanze createPflanze(){
return new Flechte();
}
public IUntergrund createUntergrund(){
return new Schnee();
}
}
Interfaces und Implementierungen der Spielweltobjekte:
//Interface
class ITier{}
class IPflanze{}
class IUntergrund{}
//Regenwald
class Elefant extends ITier{}
class Baum extends IPflanze{}
class Gras extends IUntergrund{}
//Wüste
class Kamel extends ITier{}
class Kaktus extends IPflanze{}
class Sand extends IUntergrund{}
//Polargebiet
class Eisbär extends ITier{}
class Flechte extends IPflanze{}
class Schnee extends IUntergrund{}
Welches konkrete Spielweltobjekt nun wirklich zurückgeliefert wird, entscheidet allein die Implementierung.
Was ändert sich für den Client? Er ist von der Implementierung entkoppelt und stützt sich allein auf die Abstraktion für Generator, Pflanzen, Tiere und dem Untergrund.
Client kann allgemeingültigen Spielweltcode schreiben:
public static void main(String[] args) {
//An einer zentrale Stelle wird der Typ bestimmt -> schneller Austausch
AbstractGenerator generator = new Polargebietgenerator();
//Objekte werden erstellt
IPflanze pflanze = generator.createPflanze();
ITier tier = generator.createTier();
IUntergrund untergrund = generator.createUntergrund();
//...
//Welt wird gebaut
}
Er instanziiert eine AbstractGenerator-Implementierung und entscheidet damit von welchem Typ seine Spielwelt sein soll. Nun kann er beliebig Tier, Pflanzen und den Untergrund erstellen und diese zusammensetzen.
Schauen wir uns die Vorzüge unseres neuen Entwurfs an.
In der Gesamtschau ergibt sich folgendes Klassendiagramm:
Es zeigt sich, dass durch den Einsatz des Abstract Factory Patterns, ein hohes Maß an Flexibilität und Allgemeingültigkeit gewonnen wird, während zeitgleich die Wartung erleichtert wird und Erweiterungen schnell und unkompliziert möglich sind.
Nach dieser Einführung wird im folgendem Abschnitt das Abstract Factory Design Pattern formalisiert, näher analysiert und diskutiert.
| Klasse | Abstract Factory Teilnehmer |
|---|---|
| AbstractGenerator | AbstractFactory |
| Konkrete Generatoren | Concrete Factorys |
| Spielweltobjekte (Tiere, Pflanzen, Untergrund) | Products |
| konkrete Spielweltobjekte (Elefant, Sand, Schnee, Kaktus etc.) | Concrete Products |
Abstract Factory:
"Biete eine Schnittstelle zum Erzeugen von Familien verwandter oder voneinander abhängiger Objekte, ohne ihre konkreten Klassen zu benennen."
([GoF], Seite 107)
Das Abstract Factory Design Pattern dient der Definition einer zusammenhängenden Familie aus Produkten (engl. products). Die Familien können elegant ausgetauscht werden.
Der Instanziierungscode wird in eine Factory ausgelagert. Allerdings wird die Factory hinter einer abstrakten Schnittstelle vor dem Client verborgen. Diese Factoryschnittstelle, die namensgebende Abstract Factory, definiert für jedes Produkt der Produktfamilie (Produktsatz) eine Operation, mit der der Client eine Instanz des jeweiligen Produkts erhalten kann. Der Client ist damit von einer bestimmten Factoryimplementierung entkoppelt. Es stützt sich allein auf Abstraktion - sowohl bei den Produkten als auch bei der Factory.
Da eine Factory immer die gesamte Schnittstelle erfüllen muss, wird sicher gestellt, dass der Client nur mit Produkten arbeitet, die zusammen gehören und zusammen passen.
Es wird eine Schnittstelle für die Factory und die Products definiert (hier mit den Interfaces AbstractFactory, IProduct1 etc.). Konkrete Factorys (ConcreteFactoryA, ConcreteFactoryB) realisieren die Schnittstelle der AbstractFactory und implementieren die Erstellungsmethoden. In dieser instanziiert die Factory ihre eigene Implementierung des jeweiligen Produkts und gibt sie zurück. Der Client kennt nur die Schnittstellen.
AbstractFactory und Implementierungen:
//Factory
interface AbstractFactory{
public IProduct1 createProduct1();
public IProduct2 createProduct2();
public IProduct3 createProduct3();
}
class ConcreteFactoryA implements AbstractFactory{
@Override
public IProduct1 createProduct1() {
return new ConcreteProduct1A();
}
@Override
public IProduct2 createProduct2() {
return new ConcreteProduct2A();
}
@Override
public IProduct3 createProduct3() {
return new ConcreteProduct3A();
}
}
class ConcreteFactoryB implements AbstractFactory{
@Override
public IProduct1 createProduct1() {
return new ConcreteProduct1B();
}
@Override
public IProduct2 createProduct2() {
return new ConcreteProduct2B();
}
@Override
public IProduct3 createProduct3() {
return new ConcreteProduct3B();
}
}
Productinterfaces und Implementierungen:
//Products
interface IProduct1{}
interface IProduct2{}
interface IProduct3{}
class ConcreteProduct1A implements IProduct1{}
class ConcreteProduct1B implements IProduct1{}
class ConcreteProduct2A implements IProduct2{}
class ConcreteProduct2B implements IProduct2{}
class ConcreteProduct3A implements IProduct3{}
class ConcreteProduct3B implements IProduct3{}
Beispielclient:
public static void main(String[] args) {
//zentraler Austauschpunkt -> Implementierungsaustausch
AbstractFactory factory = new ConcreteFactoryA();
IProduct1 product1 = factory.createProduct1(); //product1-implementierung von ConcreteFactoryA
IProduct2 product2 = factory.createProduct2(); //product2-implementierung von ConcreteFactoryA
IProduct3 product3 = factory.createProduct3(); //product3-implementierung von ConcreteFactoryA
}

Typische Anwendung von Abstract Factorys sind GUI-Bibliotheken, die unterschiedliche Look & Feels unterstützen. Dabei wird für jedes typische "Widget" (Textfeld, Button, Scrollpane, Panel, Labels) ein Interface definiert. Für jedes Look & Feel werden alle Widgets implementiert und eine konkrete Factory (beispielsweise WindowsFactory, MacOSXFactory oder GDKFactory) erzeugt, die Instanzen des entsprechenden Widgets zurückgibt. Die konkreten Factorys erfüllen dabei die Schnittstelle einer Abstract Factory (Methoden: createButton(), createTextField() etc). Der Client holt sich die Widgets (vom Typ des Interfaces) von der Abstract Factory, weiß dabei aber nicht, welches Widget er nun konkret erhält bzw. von welchem Look & Feel es ist. Dies ermöglicht das schreiben von GUI-Code unabhängig vom gewählten Look & Feel und macht das Look & Feel damit austauschbar (frei nach [GoF], Seite 107f; Bilderquelle: SWT).
| Exkurs: Vergleich von Abstract Factory und Factory Method | |
|---|---|
Einsteiger in der Thematik von Design Pattern fällt oft die Unterscheidung zwischen den Entwurfsmustern Abstract Factory und Factory Method schwer. |
|
| Abstract Factory | Factory Method |
Gemeinsamkeit: Beide Pattern entkoppelt den Client von konkreten Typen. Der Client kennt nur die Schnittstelle zur Factory und den Produkten. Welche Produkte konkret erstellt werden, entscheidet die Unterklasse/Implementierung der Schnittstelle. Der Client stützt sich allein auf Abstraktion. |
|
|
|
Kommentare
Die Erweitebarkeit bedeutet, z.B. neuen Drücker (Laserdrücker) in bestehendes System (Abstrakte Klasse Drücker und 2 abgeleiteten Drücker TintenDrücker und Strahldrücker) integrieren ohne bestehenden Code zu brechen.
Seite: 1 -