Warum ich das Abstrakte-Fabrik-Muster so liebe und du es auch tun sollst.

Veröffentlicht: vor 6 monaten · Lesedauer ca. 24 Minuten

Eins meiner Lieblingsmuster ist zweifelsohne die „Abstrakte Fabrik„. Allein schon deswegen, weil die so „konkret“ ist.

Hä? Etwas Abstraktes soll konkret sein? Das kann doch nicht sein. Oder doch?

Zugegeben, vergleicht man das UML-Diagramm mit den anderen Pattern, so ist das von der abstrakten Fabrik natürlich das, was am komplexesten aussieht.

Das liegt eigentlich nur daran, weil es viele Klassen und Interfaces gibt. Bereits in der einfachsten Variante haben wir zwei Interfaces und zwei Klassen.

Aber glaub mir, dieses Muster ist wirklich einfach zu verstehen und anzuwenden.

Übrigens, dieses Muster heißt zwar „Abstract Factory“, trotzdem verwende ich es mit Interfaces und nicht mit abstrakten Klassen. Einfach nur darum, weil man mehrere Interfaces in einer Klasse definieren (konkreter gesagt, implementieren) und auch weniger Schreibarbeit bei der Definition eines Interfaces hat.

Abstract Factory: Definition

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Gang of four (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)

Die Definition ist etwas schwammig formuliert. Es wird lediglich gesagt, dass Schnittstellen bereitgestellt werden, um ganze Objektstrukturen zu erstellen, ohne diese konkret zu benennen.

Oder einfacher ausgedrückt, beschreibt die abstrakte Fabrik das „was„. Jedoch nicht, „was genau„.

Und genau diese Tatsache macht es so flexibel. Vorausgesetzt, der Rest der Architektur stimmt 🙂

Abstract Factory: Problem

Folgendes Szenario:

Wir programmieren ein Spiel und implementieren 4 Waffen. Schwert, Dolch, Axt und Knüppel. Alles Einhandwaffen.

Als guter Programmierer haben wir ja schon etwas von Interfaces gehört und erstellen mal eben sowas hier:

public interface IWeapon { }
public class Sword : IWeapon { }
public class Dagger : IWeapon { }
public class Axe : IWeapon { }
public class Cudgel : IWeapon { }

Da es ein Fantasy-Spiel ist, gehören da natürlich auch ein paar Völler dazu. Also gibt es in dem Spiel (mal wieder die üblichen Verdächtigen) Menschen, Elfen und Orcs.

Um am Krieg teilzunehmen, benötigen alle Spieler eine Waffe. Im Code könnte so etwas stehen:

ICharacter elf1 = new Character(Race.Elf, Class.Warrior, Gender.Female);
elf1.Weapon = new Axe();

ICharacter elf2 = new Character(Race.Elf, Class.Warrior, Gender.Male);
elf2.Weapon = new Sword();

Soweit so gut.

Für die nächste Version sind volksspezifische Waffen angedacht, die sich durch die Größen und teilweise auch Modelle und Texturen unterscheiden.

Es werden also ganz viele neue Klassen benötigt. Und hinterher steht im Code sowas hier:

ICharacter elf1 = new Character(Race.Elf, Class.Warrior, Gender.Female);
elf1.Weapon = new OrcAxe();

ICharacter elf2 = new Character(Race.Elf, Class.Warrior, Gender.Male);
elf2.Weapon = new ElfSword();

Huch. Die zierliche kleine Elfe steht auf einmal mit einer Ork-Axt da, die so groß ist wie die kleine Frau selber und mindestens doppelt so viel wiegt.

Besser wäre natürlich sowas hier:

IArmory armory = new ElfArmory();

ICharacter elf1 = new Character(Race.Elf, Class.Warrior, Gender.Female);
elf1.Weapon = armory.GetAxe();

ICharacter elf2 = new Character(Race.Elf, Class.Warrior, Gender.Male);
elf2.Weapon = armory.GetSword();

Was haben wir dadurch gewonnen? Ganz einfach. Es gibt eine Waffenkammer, aus dem sich jeder Elfe bedienen kann. Solche Fehler wie oben können nicht mehr passieren. Mit anderen Worten: Es ist sichergestellt, dass ein Elfenschwert nur beim Elfen und eine Ork-Axt nur beim Orken landet.

Abstract Factory: Struktur

UML Klassendiagram Abstrakte Fabrik

AbstractProduct: Jedes Produkt soll zunächst als ein abstraktes Produkt definiert werden. Dabei gilt zunächst, je abstrakter, desto besser.

ConcreteProduct: Anschließend werden anhand eines abstrakten Produktes konkrete definiert. Es soll für jedes abstrakte Produkt ein konkretes Produkt aus (und jetzt kommt’s) jeder Familie definiert werden.

AbstractFactory: Eine abstrakte Fabrik, definiert zunächst eine Methode, für jedes abstrakte Produkt, welches man eigentlich erstellen möchte.

ConcreteFactory: Jede Produktfamilie, bekommt jetzt eine eigene, konkrete Fabrik. Hier werden explizit die passenden konkreten Produkte definiert.

Client: Muss sich dann nicht mehr um die Erzeugung der ganzen Produkte kümmern. Es muss dann nur noch die Frage gestellt werden „woher“?

Abstract Factory: Real-World

Um eine abstrakte Fabrik vorzustellen, kannst du dir eigentlich alles vorstellen, was in irgendeiner Art und Weise in Konkurrenz zueinander steht.

Machen wir mal ein Beispiel mit Autos, mit vielen Autos 🙂

Man möge mir verzeihen, wenn die Autos nicht immer zu 100% gleichwertig sein sollten, aber die grobe Richtung sollte klar sein, oder?

Solch ein Vorgehen machen übrigens auch die meisten – wenn nicht sogar alle – Autoverleihfirmen. Man ruft da an und bestellt sich ein Kombi.

Man bekommt auch ein Kombi. Aber nicht immer genau das Model, was man sich gewünscht hat. Es könnte ein VW sein oder ein BWM oder auch ein Opel. Eben gerade das, was da ist.

Wenn wir die Welt der ganzen Produkte, bzw. Materialisierung verlassen, können wir die abstrakte Fabrik noch an anderen Stellen in unserer Welt entdecken.

Auf allen Kontinenten dieser Erde leben verschiedene Tiere, Fleischfresser und Pflanzenfresser. Es gibt verschiedene Pflanzen und auch verschiedene Landschaften. Trotzdem haben die immer eins gemeinsam.

Ein Baum in Afrika sieht zwar anders aus als ein Baum in Kanada, trotzdem lässt er sich als Baum erkennen. Und so könnte man eine Methode unter dem Namen „CreateTree“ implementieren.

Um ein wenig in der Computerwelt zu bleiben, kannst du dir aber auch die verschiedenen Benutzersteuerelemente ansehen. Unter verschiedenen Betriebssystemen sehen diese nämlich anders aus. Aber ein „Button“ ist im Grunde genommen immer gleich.

Abstract Factory: Pseudocode

// in einer abstrakten Fabrik, gibt es verschiedene Produkte, die man erstellen
// möchte. Für jede Produktvariante soll ein abstraktes Produkt oder ein
// Interface erstellt werden
interface ProductA
interface ProductB

// anschließend werden für jede Produktausprägung eine eigene Klasse erstellt
// um die Logik voneinander zu kapseln.
class ConcreteProductA1 implement ProductA
class ConcreteProductA2 implement ProductA
class ConcreteProductB1 implement ProductB
class ConcreteProductB2 implement ProductB

// Es gibt ein allgemeines Interface (oder abstrakte Klasse) für die Fabrik, welches
// die jeweiligen Ausprägungen für die Produkte erstellen soll
interface Factory
	method createA() returns ProductA
	method createB() returns ProductB

// Eine konkrete Fabrik erstellt ein Produkt einer bestimmten Produktfamilie,
// die zu einer bestimmten Ausprägungen gehört. Die Fabrik stellt sicher, dass
// die jeweiligen Produkte alle miteinander kompatibel sind.
class ConcreteFactory1 implements Factory
	method createA() returns ProductA
		return ConcreteProductA1()
		
	method createB() returns ProductB
		return ConcreteProductB1()

// Jede Fabrik erstellt die eigenen Produktvariationen
class ConcreteFactory2 implement Factory
	method createA() returns ProductA
		return ConcreteProductA2()
		
	method createB() returns ProductB
		return ConcreteProductB2()
		
// Die eigentliche Logik braucht sich jetzt nicht mehr um die einzelnen
// Ausprägungen zu kümmern. Es wird darauf vertraut, dass die Produkte sich
// gleich verhalten.
// Dabei spielt es keine Rolle, welche Variationen schlussendlich verwendet 
// werden, solange die gemeinsame Schnittstelle klar definiert ist.
class Context
	field productA
	field productB
	
	constructor (factory)
		this.productA = factory.createA()
		this.productB = factory.createB()
	
	method operation()
		this.productA.operation()
		this.productB.operation()

program Client
	// Das Program muss jetzt nur noch entscheiden, welche Variationen verwendet
	// werden sollen.
	context = new Context(ConcreteFactory1)
	context.operation()	// A1 + B1
	// die gleiche implementierung kann auch mit einem anderen Interface 
	// verwendet werden
	context = new Context(ConcreteFactory2)
	context.operation()	// A2 + B2
	// Die Factory kann aber auch direkt verwendet werden ohne den Context zu
	// verwenden.
	factory = new ConcreteFactory1()
	productA = factory.createA()	// A1

Abstract Factory: Implementation

Im Grunde genommen kannst du das komplette Muster in fünf Schritten abbilden:

  1. Erstelle eine Interface für jedes Produkt, welches du definieren möchtest (AbstractProduct).
  2. Erstelle ein Interface für die Fabrik und definiere eine Methode zum Erstellen der Produkte breit. Dabei muss für jedes Produkt (mindestens) eine Methode definiert werden (AbstractFactory).
  3. Erstelle für jede mögliche Variation eine eigene Klasse und definiere diese als Fabrik (ConcreteFactory)
  4. Erstelle eine Klasse für jede Produktausprägung und deklariere diese als ein davor definiertes Produkt. Hierbei muss das Produkt in jeder möglichen Variation definiert werden (ConcreteProduct)
  5. Erstelle in den jeweiligen Fabriken ein neues Objekt der gegebenen Klasse und gib diese zurück.

Abstract Factory: Use Case

Das abstract factory pattern, kann man sehr individuell einsetzen. Beispielsweise überall, wo Daten aus verschiedenen Quelle gelesen werden können.

Wenn wir schon beim Thema lesen sind, Datenbankverbindungen sind ein Paradebeispiel für das Pattern. Der Entwickler implementiert munter gegen SQLite. Im Produktiveinsatz kommt dann irgendwas Serverseitiges zum Einsatz (bswp.: MSSQL oder MariaDB) und für UnitTests kann man das ganze noch wunderbar mocken.

Möchte man für die Applikation verschiedene Themes (z.B. hell & dunkel) erstellen, kann das Ebenfalls mit diesem Muster gelöst werden. Es wird einfach eine Factory erstellt, die die Farben etc. bestimmt. Abhängig von der Benutzerwahl werden dann entweder die dunklen oder die hellen Elemente geladen.

Hast du schon einmal was von Datentransformation gehört?

Das ist, wenn man Daten aus einem Format in ein anderes umwandeln möchte. Doch wie kann uns dieses Pattern dabei behilflich sein?

Nun ja, ganz einfach 🙂

Stell dir mal vor, es wäre eine Anforderung Daten dynamisch aus einem Format in ein anderes zu konvertieren. Sowas wie ein Übersetzungsbüro.

Da wären unter anderem Formate wie: JSON, XML, SQLite, Excel, csv, ini, etc.pp.

Wenn du jetzt anfängst und für jede Kombination ein Transformator schreibst, benötigst du bereits 30 verschiedene Konverter. Das ist nicht nur viel, sondern auch sehr fehleranfällig.

Besser ist es ewas zu definieren, womit alle umgehen können. Anschließend muss für jedes einzelne Format nur noch 2 Klassen (oder Komponenten) geschrieben werden. Eins zum Lesen und eins zum Schreiben.

Ich persönlich verwende es sehr gerne, um Objekte zu erstellen und an einer Stelle zentral zu kapseln. Meistens habe ich auch nicht 2, 3 oder 4 Factories, sondern nur eine. Ich bin dann auf mögliche Eventualitäten vorbereitet und kann trotzdem ziemlich dynamisch Objekte erstellen.

Ich muss auch nicht mehr alle Klassen auswendig kennen. Oder mich auf etwas festlegen.

Ist doch viel praktischer, oder? 🙂

Abstract Factory: Vorteile

Single Responsibility Prinzip

Klar. Jedes einzelne Produkt hat genau eine Aufgabe. Jede Klasse hat genau eine Aufgabe.

Und dabei ist natürlich alles schön voneinander gekapselt. Was mich schon zum nächsten Punkt bringt.

Open/Closed Prinzip

Wenn neue Produkte hinzukommen, werden die alten Produkte nicht angefasst. Das gewährt uns die Sicherheit, dass wir keine Fehler in bereits bestehenden Quellcode einschleusen.

Kann ja immer mal vorkommen (bei anderen Programmierern natürlich. Nicht bei dir :-))

Lose Kopplung

Sollten Produkte voneinander Wissen? Natürlich nicht! Und schon gar nicht von ihren Zwillingen aus anderen Produktfamilien.

Bei der Verwendung von diesem Muster, hast du quasi keine Chance eine starke Kopplung zu erreichen. Dafür musst du dich schon sehr anstrengen (aber ja, es geht!).

Kompatibilität

Auch wenn die Bereitstellung etwas komplex ist, ist die Verwendung dieses Musters ein Traum. Man muss sich nur am Anfang entscheiden, was man haben möchte. Und im weiteren Verlauf bekommt man fertige Objekte.

Und zwar solche, die allesamt zueinander passen. Man muss noch nicht einmal darüber nachdenken, ob die Verwendung jetzt mögliche Implikationen auf andere Bereiche haben könnte. Nein, dieses Muster garantiert die Kompatibilität.

Ebenso wird man auch gezwungen Interfaces (bzw. abstrakten Klassen) in seinem Quellcode zu verwenden. Bereits die Entwicklungsumgebung wart dich ja bereits, dass ein konkretes Produkt verwendet wurde und nicht das Interface. Sollten trotzdem (aus irgendwelchen Gründen auch immer) konkrete Produkte verwendet werden, kann man an dieser Stelle die Funktionalität nicht mehr austauschen und das ganze Muster ist hin.

Sehr gut Testbar

Muss ich dazu noch etwas sagen? Ich glaube nicht 🙂

UnitTests FTW! 🙂

Abstract Factory: Nachteile

Auch hier gibt es wieder einige Nachteile, die ich dir nicht verschweigen möchte:

Viele Klassen

Dadurch, dass wir für jede einzelne Variante und für jedes einzelne Produkt eine Klasse benötigen, steigt der Bedarf an Klassen natürlich enorm an.

Allein schon bei 3 Fabriken und nur 5 Produkten, sind es bereits 15 Klassen alleine für jede Produktausprägung. Dazu kommen natürlich noch die Interfaces und die Factory-Klassen hinzu. Bei diesem Beispiel sind es insgesamt 24 Dateien. Wenn ein Produkt hinzukommt, sind es bereits 28 Dateien. Kommt noch eine weitere Fabrik hinzu, dann sind es bereits 35 Dateien bzw. Typen.

35 verschiedene Typen für 4 Fabriken und 6 Produkte.

35!!! In Worten: fünfunddreißig.

Ich glaube du verstehst worauf ich hinaus möchte :-). Diese immense Anzahl an verschiedenen Typen macht nicht nur die Strukturierung etwas unübersichtlich. Nein, man kann auch schnell mal den Überblick verlieren.

Schwierig zu erweitern

Stellen wir uns mal vor, es kommt eine neue Anforderung und wir müssen ein neues Produkt in unsere Infrastruktur hinzufügen.

Dabei reicht es dann natürlich nicht aus, wenn man nur ein konkretes Produkt anlegt. Nein, es muss das Interface für die abstrakte Fabrik angepasst werden. Dann müssen alle konkreten Fabriken angepasst werden. Und was sollen die zurückgeben? Richtig! Es müssen viele neue Dateien hinzugefügt werden und die Funktionalität ebenfalls implementiert werden.

Sollte eine neue Fabrik hinzukommen, gilt das natürlich ebenso. Für jede neue Produktausprägung muss eine Klasse hinzugefügt werden und ebenfalls ausprogrammiert werden.

Natürlich darf man dabei auch nicht die UnitTests vergessen, um eine möglichst hohe Code-Coverage zu erreichen 🙂

YAGNI

YAGNI ist nicht direkt ein Nachteil. Es ist (ähnlich wie SOLID) ein Programmierprinzip.

Dabei ist YAGNI einfach eine Abkürzung für „You Aren’t Gonna Need It“ oder auf Deutsch: „Du wirst es nie wieder brauchen“.

Und tatsächlich birgt dieses Pattern auch diese Gefahr. Man implementiert verschiedene Produkte für eine Produktfamilie. Im Anschluss überlegt man sich, eine weitere Familie hinzuzufügen. Jetzt müssen natürlich alle Produkte noch einmal entwickelte werden. Und im schlimmsten Fall benötigt man diese Produkte gar nicht.

Ebenso, wenn man nur ein Produkt aus einer bestimmten Familie benötigt. Man muss alle Familien berücksichtigen. Auch solche, die man ggf. nie mehr im Leben mehr benötigt.

Abstract Factory: Konkretes Beispiel

Jetzt habe ich schon viel darüber erzählt die toll dieses Muster ist, aber auch wie umständlich es bei der Erweiterung ist.

Jetzt möchte ich noch gerne mit dir zusammen einmal das Pattern an einem ganz konkreten Beispiel implementieren.

Und zwar möchte ich das Eingangs erwähnte Beispiel etwas weiter ausprogrammieren.

Wir haben also vier Waffen und drei Völker. Also haben wir bereits nur für die Waffen 12 unterschiedliche Klassen.

Dazu kommen noch drei volksspezifische „Waffenkammern“ (also Factories) dazu.

Abstrakte Produkte / Waffen

Bevor ich loslege, definiere ich mir zunächst einmal alle Produkte.

Hierfür kann ich noch eine übergeordnete Klasse erstellen, indem ich alle gemeinsamen Funktionalitäten aller Produkte (in diesem Fall Waffen) noch einmal definiere.

public abstract class Weapon 
{
	public float MinDamage { get; set; }
	public float MaxDamage { get; set; }
}

public abstract class Sword : Weapon { }

public abstract class Dagger : Weapon { }

public abstract class Axe : Weapon { }

public abstract class Cudgel : Weapon { }

Konkrete Produkte

Nun haben wir ja 4 Waffen und 3 Völker. Um hier einfach etwas Platz zu sparen, möchte ich nur ein paar Produkte genauer implementieren. Ich glaube jedenfalls nicht, dass du jetzt ganz lange Scrollen möchtest, um ab hier zur nächsten Passage zu kommen 🙂

Wenn du trotzdem jede einzelne Waffe sehen möchtest, kannst dies gerne hier tun 🙂

public class OrcSword : Sword { }

Abstrakte Fabrik

Um die Waffen zu erstellen, benötigen wir wiederum eine abstrakte Klasse, die alle möglichen Waffen definieren kann.

public abstract class Armory
{
    public abstract Sword GetSword();
    public abstract Dagger GetDagger();
    public abstract Axe GetAxe();
    public abstract Cudgel GetCudgel();
}

Wie man sieht, wurden bis hierher noch keine konkreten Typen benannt. Das geht auch nicht anders. Andernfalls würden Inkonsistenzen auftreten und man hätte doch noch ein Produkt aus Famile A in Familie B drin.

Konkrete Fabrik

Erst jetzt wird es spezifisch.

Auch hier, mache ich nur ein Beispiel. Die anderen beiden Fabriken, sehen genauso aus. Jedoch mit den entsprechend anderen Waffen.

public class HumanArmory : Armory
{
    public override Sword GetSword() => new HumanSword();
    public override Dagger GetDagger() => new HumanDagger();
    public override Axe GetAxe() => new HumanAxe();
    public override Cudgel GetCudgel() => new HumanCudgel();
}

Und in den meisten Fällen kommt da auch nicht sonderlich mehr dazu. Es sei denn, es werden mehr Produkte.

Die Hauptaufgabe der Factory ist einfach nur, Objekte zu erstellen. Es ja auch schließlich ein Erzeugungsmuster.

Übrigens, man könnte jetzt noch eine weitere Factory (entweder als Factory Method oder Abstract Factory) einbauen um an die eigentliche Factory ranzukommen. Das würde dann ungefähr so aussehen:

public class ArmoryFactory
{
    public Armory ForHumans() => new HumanArmory();
    public Armory ForElves() => new ElfArmory();
    public Armory ForOrcs() => new OrcArmory();
}

Idealerweise noch mit einem Interface IArmoryFactory. Das muss aber nicht zwingend notwendig sein.

Business Logik

Die Hauptaufgabe ist getan.

Jetzt können wir die komplette, abstrakte Fabrik verwenden. In der Geschäftslogik brauchen wir auch nur noch die Abstraktion.

Ein Beispiel könnte dann in etwa so aussehen:

public class Context
{
    private Abstract.Armory _armory;

    public Context(Abstract.Armory armory)
    {
        _armory = armory;
    }

    public void CheckWeapons()
    {
        Sword sword = _armory.GetSword();
        Console.WriteLine(GetWeaponName(sword));

        var dagger = _armory.GetDagger();
        Console.WriteLine(GetWeaponName(dagger));

        var axe = _armory.GetAxe();
        Console.WriteLine(GetWeaponName(axe));

        var cudgel = _armory.GetCudgel();
        Console.WriteLine(GetWeaponName(cudgel));
    }

    private string GetWeaponName(Weapon weapon)
    {
        return weapon.GetType().Name;
    }
}

Und wie sieht dann die Verwendung davon aus?

Hier ein paar Beispiele dazu 🙂

var humans = new Context(factory.ForHumans());
humans.CheckWeapons();

Die passende Ausgabe zu diesem Quellcode gibt es dann hier 🙂

Um nicht zu viel vorwegzunehmen: die Ausgabe ist natürlich so wie erwartet 😉

Abstract Factory: Code Beispiel

Die Code-Beispiele sind jeweils bei github zu finden.

Abstract Factory: Verwandte Muster

Häufig startet man mit einer normalen Fabrikmethode. Wenn die Anforderung wachsen, entsteht daraus eine abstrakte Fabrik. Hat man bereits einige abstrakte Fabriken, kann man diese wiederum in einer Fabrikmethode „kapseln“.

Unterschiedliche Factories, lassen sich sehr gut als Bridge verwenden (wie im oberen Beispiel). Diese Kombination ist übrigens extrem mächtig, da die abstrakte Fabrik die ganze Erzeugungslogik auslagert und die Brücke nur mit den Abstraktionen arbeitet.

Eine abstrakte Fabrik kann auch die Fassade ersetzen, solange du nur die Objektdefinitionen innerhalb eines Systems verbergen möchtest.

Abstract Factory: Das Fazit

Wie ich bereits erwähnt habe, finde ich persönlich, dass die abstrakte Fabrik ein ziemlich geniales Muster ist.

Zugegeben, man braucht viele, viele Klassen um dieses Muster abzubilden. Aber auf der anderen Seite hätte man diese sowieso. Jedenfalls die ganzen „konkreten“ Klassen. Hinzu kommen dann noch ein paar Klassen für die Factory selber und fertig ist das ganze Konzept.

Häufig ist es auch zu sehen, dass man erst einmal mit „normalen“ Factories anfängt. Also bloß mit einer einzigen. Nach und nach kommen da jedoch weitere Ausprägungen hinzu und man erreicht einen gewissen Abstraktionsgrad.

Und fertig ist unsere „Abstract Factory“!

Kategorien:
Design Pattern
Das könnte dir auch gefallen
Sei der erste und teile deine Meinung mit der Welt!
... und was meinst du dazu?
Deine E-Mail-Adresse wird nicht veröffentlicht.