Das Abstract Factory Design Pattern

Studienprojekt von Philipp Hauer. 2009 - 2010. ©

Inhalt

Einführung

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.

Abstract Factory Einleitung

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:

Abstract Factory Einleitung

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:

  • Inkonsistenz. Der Client kann nun die Welten beliebig miteinander mischen. Dies ist nicht vorgesehen, da die Spielweltobjekte, nur mit Objekten der selben Welt interagieren und funktionieren können. Ein Kamel kann auf Schnee nicht gehen und unser Baum wird nicht lange auf trockenem Sand überleben. Unser "Framework" ermöglicht Inkonsistenz und damit Fehlfunktionen.
  • Keine Allgemeingültigkeit. Der Client kann keinen allgemeingültigen Code schreiben. Hat er Code für eine Polargebietwelt geschrieben und will nun die gleiche Anzahl von Tieren und Pflanzen bei identischem Arrangement in eine Regenwaldwelt übertragen, so muss sein Code komplett umgeschrieben werden, damit sie mit den Klassen des Regenwaldes arbeiten. Ein Wartungsalptraum. Eine unbefriedigende Alternative wäre die zahlreichen Nutzung von if-else-Konstrukten, um zwischen dem gewünschten Generator und Welttyp auszuwählen. Der Code wird unnötig größer, unübersichtlicher, fehlerträchtiger und damit schlechter wartbar.
  • Keine Erweiterbarkeit. Begründet liegt obiger Punkt in der Tatsache, dass der Client mit konkreten Klassen arbeitet, sowohl bei den Generatoren als auch bei den Spielweltobjekten. Dies führt zu einer engen Kopplung. Soll nun ein Generator angepasst werden (zum Beispiel Polargebietgenerator: statt Eisbären sollen nun Schneeleoparden als Tiere erzeugt werden) bricht der Clientcode und Wartung wird erforderlich. Eine Factory, die konkrete Klassen zurückgibt, ist aus diesem Grund sinnlos. Sie verfehlt den Zweck von Factorys: Flexibler Austausch von Implementierungen.

Unser 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 Pflanze, Tier und Untergrund ein.

Abstract Factory Einleitung

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 (Pflanze, Tier, Untergrund) zurück.

Abstract Factory Einleitung

Code des Interfaces AbstractGenerator und dessen Implementierungen Regenwald-, Wüsten und Polargebietgenerator:

interface AbstractGenerator{
    public Tier createTier();
    public Pflanze createPflanze();
    public Untergrund createUntergrund();
}

class Regenwaldgenerator implements AbstractGenerator{
    public Tier createTier(){
        return new Elefant();
    }
    public Pflanze createPflanze(){
        return new Baum();
    }
    public Untergrund createUntergrund(){
        return new Gras();
    }
}
class Wüstengenerator implements AbstractGenerator{
    public Tier createTier(){
        return new Kamel();
    }
    public Pflanze createPflanze(){
        return new Kaktus();
    }
    public Untergrund createUntergrund(){
        return new Sand();
    }
}
class Polargebietgenerator implements AbstractGenerator{
    public Tier createTier(){
        return new Eisbär();
    }
    public Pflanze createPflanze(){
        return new Flechte();
    }
    public Untergrund createUntergrund(){
        return new Schnee();
    }
}			
Interfaces und Implementierungen der Spielweltobjekte:

//Interface
interface Tier{}
interface Pflanze{}
interface Untergrund{}

//Regenwald
class Elefant implements Tier{}
class Baum implements Pflanze{}
class Gras implements Untergrund{}

//Wüste
class Kamel implements Tier{}
class Kaktus implements Pflanze{}
class Sand implements Untergrund{}

//Polargebiet
class Eisbär implements Tier{}
class Flechte implements Pflanze{}
class Schnee implements Untergrund{}			

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 
    Pflanze pflanze = generator.createPflanze();
    Tier tier = generator.createTier();
    Untergrund 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.

  • Flexibilität und Allgemeingültigkeit durch Entkopplung des Client von konkreten Implementierungen. Der Typ der Spielwelt wird an einer zentralen Stelle festgelegt: bei der einmaligen Instanziierung des gewünschten Generators (beispielsweise Regenwaldgenerator). Fortan wird abstrakter, allgemeingültiger Code geschrieben. Der Client weiß nicht, dass er in Wirklichkeit mit Bäumen, Elefanten und Gras arbeitet. Das ermöglicht das denkbar einfache Austauschen des Generators und damit der gesamten Spielweltobjektfamilie. So kann ein Wüstengenerator stattdessen instanziiert werden und schon baut der Client, ohne es zu wissen, seine Welten mit Kakteen, Kamelen und Sand.
  • Erweiterbarkeit und Wartbarkeit. Aus diesem Grund können auch schnell neue Welten (beispielsweise Steppe) ins System integriert werden. Dazu muss lediglich der AbstractGenerator implementiert werden und die eigenen Spielweltobjekte, die wiederum die entsprechenden Interfaces realisieren, zurückgeben werden. Alles ohne bestehenden Code zu brechen.
  • Wartbarkeit. Auch können leicht Änderungen an bestehenden Spielwelten durchgeführt werden. Soll es im Regenwald nun Tiger statt Elefanten geben, muss lediglich die createTier()-Methode des Regenwaldgenerators angepasst werden. Schon tummeln sich in allen bisher gebauten Welten (Clientcode) Tiger. Es entsteht kein Änderungsaufwand am Client, dank allgemeingültigen, auf Abstraktion gestützten Code.
  • Konsistenz. Jetzt stellt unser kleines Spielweltframework sicher, dass nur Spielweltobjekte erstellt werden, die auch zueinander passen und miteinander funktionieren. Kapselt man nun noch die Instanziierung der Generatorklasse im Framework (beispielsweise in einer eigenen Factory) und überlässt sie nicht dem Client (zur Zeit könnte er noch mehrere Generatoren erstellen und die Objekte damit mischen), so wird das höchste Maß an Konsistenz sichergestellt.

In der Gesamtschau ergibt sich folgendes Klassendiagramm:

Abstract Factory Gesamtschau

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.

Das Spielweltgeneratorbeispiel mit Abstract Factory Pattern Termini

Klasse Abstract Factory Teilnehmer
AbstractGenerator AbstractFactory
Konkrete Generatoren Concrete Factorys
Spielweltobjekte (Tiere, Pflanzen, Untergrund) Products
konkrete Spielweltobjekte (Elefant, Sand, Schnee, Kaktus etc.) Concrete Products

Analyse und Diskussion

Gang Of Four-Definition

Abstract Factory:
"Biete eine Schnittstelle zum Erzeugen von Familien verwandter oder voneinander abhängiger Objekte, ohne ihre konkreten Klassen zu benennen."
([GoF], Seite 107)

Beschreibung

Das Abstract Factory Design Pattern Beschreibung

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.

Abstract Factory Pattern

AbstractFactory und Implementierungen:

//Factory
interface AbstractFactory{
    public Product1 createProduct1();
    public Product2 createProduct2();
    public Product3 createProduct3();
}

class ConcreteFactoryA implements AbstractFactory{
    @Override
    public Product1 createProduct1() {
        return new ConcreteProduct1A();
    }

    @Override
    public Product2 createProduct2() {
        return new ConcreteProduct2A();
    }

    @Override
    public Product3 createProduct3() {
        return new ConcreteProduct3A();
    }
}

class ConcreteFactoryB implements AbstractFactory{
    @Override
    public Product1 createProduct1() {
        return new ConcreteProduct1B();
    }
    
    @Override
    public Product2 createProduct2() {
        return new ConcreteProduct2B();
    }
    
    @Override
    public Product3 createProduct3() {
        return new ConcreteProduct3B();
    }
}			
Productinterfaces und Implementierungen:

//Products
interface Product1{}
interface Product2{}
interface Product3{}

class ConcreteProduct1A implements Product1{}
class ConcreteProduct1B implements Product1{}

class ConcreteProduct2A implements Product2{}
class ConcreteProduct2B implements Product2{}

class ConcreteProduct3A implements Product3{}
class ConcreteProduct3B implements Product3{}			
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
}			

Anwendungsfälle

  • Wenn verschiedene Objekte zu einem Kontext erstellt werden und daher immer zusammenhängend erstellt werden müssen. Also immer dann, wenn Konsistenz im Objektpool sichergestellt werden muss.
    • In einem System müssen Dateien verschiedenen Formats eingelesen und unterschiedlich weiterverarbeitet werden. Dafür wird für jedes Format eine Reader-Klasse erstellt und für jede mögliche Weiterverarbeitung eine Transformer-Klasse. Dabei ist es unbedingt erforderlich, dass zu einem Reader auch der passende Transformer gewählt wird. Um dieser Anforderung zu genügen, kann das Abstract Factory verwendet werden. (nach [PK], Seite 21)
    • Die Persistenzlogik einer Applikation kapselt den Zugriff auf die Datenbank. Dabei soll diese natürlich nicht nur auf einer MySQL-Datenbank ihre Daten ablegen können, sondern auch mit anderen Datenbanken (Oracle etc.) interagieren können. Dazu sind allerdings verschiedene DBConnection- und jeweils korrespondierende DBCommand-Objekte von Nöten. Nun könnte nach dem Abstract Factory Pattern eine OracleDBClientFactory zwei Methoden zum Erhalten von solchen Objektpaaren bereitstellen. Die Persistenzlogik arbeitet auf Interfaces und hat keine Kenntnis von den verwendeten datenbankspezifischen Objekten. (frei nach [Mosa])
  • Wenn ein System mit unterschiedlichen Sets von Objekten konfiguriert werden muss oder dies ermöglich sein soll.
  • Wenn ein System losgelöst davon sein soll, wie bestimmte Objekte erstellt werden.
  • Eine Objektfamilie bereitgestellt werden soll, aber noch keine Aussagen zu den konkreten Implementierungen gemacht werden kann oder soll. Stattdessen werden Interfaces bereitgestellt.

Abstract Factory Anwendung GUI Vista Look and FeelAbstract Factory Anwendung GUI Mac Look and FeelTypische 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).

Vorteile

  • Durch das Abschirmen der konkreten Klassen wird der Clientcode allgemeingültig. Es ist kein Code für spezielle Fälle notwendig.
  • Konsistenz. Es wird sichergestellt, dass nur jene Objekte zum Client gelangen, die auch zusammenpassen. Es ist weiterhin gewährleistet, dass immer nur ein konkretes Familienmitglied eines Typs zur gleichen Zeit im Einsatz ist.
  • Flexibilität. Ganze Objektfamilien können ausgetaucht werden, ohne dass der Clientcode bricht, da sich der Client nur auf Abstraktionen (Abstract Factory, Productschnittstellen) stützt.
  • Einfache Erweiterung mit neuen Produktfamilien. Neue Productsets können sehr einfach ins System integriert werden. Dazu ist lediglich das erneute Implementieren der Factoryschnittstelle nötig. Anschließend muss nur noch an einer zentralen Stelle im Client die neue Factory instanziiert werden.
  • Wiederverwendbarkeit. Konkrete Products können Mitglieder verschiedener Produktfamilien sein.
  • Verschlankung des Clientcodes.

Nachteile

  • Unflexibilität hinsichtlich neuer Familienmitglieder. Soll der Produktfamilie ein neues Produkt hinzugefügt werden, so ist eine Änderung der Schnittstelle der Abstract Factory notwendig. Dies führt aber zum Brechen von Code aller konkreten Factorys. Der Änderungsaufwand ist groß. Daher sollte gleich zu Beginn sehr genau überlegt werden, welche Produkttypen erstellt werden sollen. Je weiter das System zum Zeitpunkt der nötigen Erweiterung fortgeschritten ist, umso mehr Code muss modifiziert werden.

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.

  • Ganze Produktfamilie wird erstellt.
  • Breite Schnittstelle
  • Schnittstelle (Abstract Factory) ist oft nur ein Interface und enthält keinen Implementierungscode.
  • Benutzt Factory Methods, aber ohne generischen Code.

 

  • Ein Produkt(typ) wird erstellt.
  • Schmale Schnittstelle
  • Schnittstelle ist oft abstrakt und enthält generischen Code (Herstellungscode, den alle Produkte durchlaufen müssen)