Das (abstract) Factory Pattern einfach erklärt!

Keylearnings:

  • Wie funktioniert das Factory Pattern?
  • Wie du das Factory Pattern in Java implementierst.
  • Was ist das abstract Factory Pattern?
  • Was ist Reflection?

Kürzlich habe ich einen Küchenschrank gekauft.

Die Möbellieferung bestand gefühlt aus etwa 1243242 Einzelteilen verpackt in kleinen Tüten.

Man war das eine Maloche den zusammenzubauen und verdammt war ich froh, dass wenigstens meine Kaffeemaschine an einem Stück geliefert wurde.

Hätte ich auch die noch selber zusammenlöten müssen, hätte das für mich nie wieder Kaffee bedeutet. Eine Katastrophe!

Mit einem ganz ähnlichen Problem sind wir in der objektorientierten Programmierung konfrontiert.

Normalerwiese arbeiten wir auch hier nicht mit atomaren Objekten, sondern in der Regel setzen sich unsere Objekte wiederum aus anderen Objekten zusammen.

Ziemlich genau so wie sich ein Küchenschrank aus den Objekten Schublade, Schranktüren und Arbeitsplatte zusammensetzt.

Und natürlich haben wir auch in unseren Programmen keine große Lust uns um den Zusammenbau dieser Objekte zu kümmern. Wir wollen diese lediglich möglichst einfach zur Verfügung gestellt bekommen und verwenden.

Also was machen wir?

Wir beauftragen eine Fabrik, die für uns das Erzeugen von Objekten übernimmt.

Und woher bekommen wir diese?

Genau das erklärt uns das Factory Pattern, das wir uns in diesem Artikel genauer ansehen wollen.

Im Groben funktioniert das ganze so:

Benötigen wir ein bestimmtes Objekt, so erteilen wir unserer Fabrik einen Auftrag, welche anschließend das angeforderte Objekt erzeugt und uns zur Verfügung stellt, ohne dass wir wissen müssen wie es erzeugt wurde.

Wie bei so vielem in der objektorientierten Programmierung, ist auch beim Factory Pattern die Verbergung von Komplexität der Ausschlag gebende Punkt.

Natürlich produziert eine Möbelfabrik nicht nur Küchenschränke sondern auch Wohnzimmer- und Kleiderschränke. Deshalb können wir der gleichen Fabrik Aufträge für unterschiedliche Objekte einer Kategorie (hier Schrank) erteilen.

Das Know How wie ein Objekt erzeugt werden muss, ist gekapselt und liegt vollständig in der Fabrik.

Wie funktioniert das Factory Pattern?

Anders als das Strategy Pattern, welches zu den behavioral Patterns gehört, fällt das Factory Pattern in die Klasse der creational Patterns.

Okay, machen wir uns die Hände dreckig und implementieren unsere Schrankfabrik.

factory pattern

Wie realisieren wir unsere Fabrik?

Unsere Fabrik dient lediglich als Hilfsmittel um neue Objekte zu erzeugen.

Im ersten Schritt müssen wir festlegen, welche Art von Objekten wir in unserer Fabrik erstellen wollen.

In unserem Beispiel soll unsere Fabrik Schränke produzieren wobei jeder produzierte Schrank eine konkrete Ausprägung wie Küchen-, Wohnzimmer- oder Kleiderschrank hat.

Mit anderen Worten: Wir wollen keine direkten Instanzen eines Schrankes erzeugen.

Um dies sicherzustellen müssen wir unseren Schrank entweder als abstrakte Klasse oder als Java Interface definieren.

Da wir das Beispiel übersichtlich halten wollen und daher keine Attribute im abstrakten Objekt Schrank benötigen, entscheiden wir uns für die Verwendung eines Interfaces. Der Einsatz einer abstrakten Klasse wäre aber ebenfalls möglich.

Das Interface enthält alle Methoden, welche die von der Fabrik produzierten Produkte enthalten soll. Wir beschränken uns auf  die Methode oeffneSchrank(), mit der wir unseren frisch produzierten Schrank öffnen können.

public interface Schrank {
	
	void oeffneSchrank();

}

Wie realisieren wir unsere Fabrik-Produkte?

Du kannst es dir sicher schon denken. Für die Implementierung der von unserer Fabrik hergestellten Objekte brauchen wir jeweils eine Klasse.

Für unsere Schrankfabrik benötigen wir also die Klassen Kuechenschrank, Wohnzimmerschrank und Kleiderschrank.

Da wir die Instanzen dieser Klassen später in einer Variablen vom Typ Schrank speichern wollen, müssen alle Klassen das Interface Schrank implementieren.

Beginnen wir mit der Klasse für den Küchenschrank. Dem Küchenschrank spendieren wir ein Attribut anzSchubladen, in dem wir die Anzahl der Schubladen unseres Küchenschranks speichern können.

class Kuechenschrank implements Schrank{
	
	private int anzSchubladen;

	@Override
	public void oeffneSchrank() {
		System.out.println("Öffne Küchenschrank");
	}

	public int getAnzSchubladen() {
		return anzSchubladen;
	}

	public void setAnzSchubladen(int anzSchubladen) {
		this.anzSchubladen = anzSchubladen;
	}
}

Machen wir weiter mit dem Wohnzimmerschrank. Ein guter Wohnzimmerschrank hat natürlich eine Glastür, deshalb benötigt unsere Wohnzimmerschrank Klasse ein Attribut glastuerrGroeße, in welchem die Größe der Glastür gespeichert werden kann.

public class Wohnzimmerschrank implements Schrank {
	
	private int glastuerGroeße;

	@Override
	public void oeffneSchrank() {
		System.out.println("Öffne Wohnzimmerschrank!");
	}

	public int getGlastuerGroeße() {
		return glastuerGroeße;
	}

	public void setGlastuerGroeße(int glastuerGroeße) {
		this.glastuerGroeße = glastuerGroeße;
	}
}

Als letztes fehlt noch der Kleiderschrank in unserem Sortiment. Der Kleiderschrank besitzt als Attribut die Anzahl der zur Verfügung stehenden Kleiderbügel anzahlKleiderbuegel.

public class Kleiderschrank implements Schrank{
	
  private int anzahlKleiderbuegel;

  public int getAnzahlKleiderbuegel() {
	return anzahlKleiderbuegel;
  }

  public void setAnzahlKleiderbuegel(int anzahlKleiderbuegel) {
	this.anzahlKleiderbuegel = anzahlKleiderbuegel;
  }

  @Override
  public void oeffneSchrank() {
	System.out.println("Öffne einen Kleiderschrank!");
  }
} 

In der zu überschreibenden Methode oeffneSchrank aus dem Schrank Interface geben wir in jeder Klasse eine Meldung auf dem Bildschirm aus, die uns Auskunft darüber erteilt um welche Schrankart es sich handelt.

Wie erteilen wir unserer Fabrik Aufträge?

Klären wir als nächstes die Frage, wie wir unsere Möbelobjekte erzeugen können.

Easy! Versuchen wir es auf die einfachste Art.

public static void main(String[] args) {
   Schrank schrank = new Kleiderschrank();
   schrank.oeffneSchrank();
}

Wenn wir das Programm starten, erhalten wir die Ausgabe.

Öffne einen Kleiderschrank!

Läuft also alles Prima! Oder?

Zugegeben es scheint irgendwie alles zu funktionieren. Aber bitte denke daran, wenn du professionell Software entwickeln möchtest, dann reicht es nicht aus, dass dein Programm tut was es soll. Sondern dein Code muss auch wartbar und erweiterbar sein.

Und genau hier liegt bei unserem Code das Problem. Wenn wir anstatt eines Kleiderschranks ein Wohnzimmerschrank erzeugen möchten, dann müssen wir den Code in der main Methode anpassen was gegen das Gesetz des „Open Closed Prinzip“ verstößt.

Die Lösung ist eine SchrankFactory!

Um dieses Problem zu lösen führen wir eine weitere Klasse ein. Nämlich eine SchrankFactory.

1: public class SchrankFactory {

2:  public static Schrank getSchrank(String schrankArt) {
3:	if (schrankArt.equals("Kleiderschrank")){
4:		return new Kleiderschrank();
5:	}
6:	if (schrankArt.equals("Kuechenschrank")){
7:		return new Kuechenschrank();
8:	}
9:	if (schrankArt.equals("Wohnzimmerschrank")){
10:		return new Wohnzimmerschrank();
11:	}
12:	return null;
13:  }
14: }

Die Klasse SchrankFactory enthält lediglich eine statische Methode getSchrank, die in Abhängigkeit des String-Parameters schrankArt entweder eine Kleiderschrank, eine Kuechenschrank oder eine Wohnzimmerschrank Instanz zurückliefert.

Okay, ändern wir unser Hauptprogramm so ab, dass die SchrankFactory Verwendung findet.

public static void main(String[] args) {
  Schrank schrank = SchrankFactory.getSchrank("Kleiderschrank");
  schrank.oeffneSchrank();
}

Wir müssen hierzu lediglich new Kleiderschrank() durch SchrankFactory.getSchrank("Kleiderschrank") austauschen.

Starten wir unser Programm, dann erhalten wir die selbe Ausgabe wie vorher.

Öffne einen Kleiderschrank!

Immer noch nicht besser!

Und was hat uns das jetzt gebracht? Garnix!

Um einen Wohnzimmerschrank anstatt eines Kleiderschranks zu kreieren müssen wir nach wie vor den Code ändern.

Anstatt wie oben new Kleiderschrank() durch new Wohnzimmerschrank zu ersetzen, müssen wir jetzt den Parameter der Methode von Kleiderschrank auf Wohnzimmerschrank ändern. Das macht es nicht wirklich besser!

Aber Reflection und ein Config-File retten uns!

Um dieses Problem zu umgehen verwenden wir eine Konfigurationsdatei und eine Java Technik, die sich Reflection nennt.

Unklar? Mir auch! Aber eine Demonstration sagt mehr als 1000 Worte.

Also auf geht’s!

Stellen wir unsere SchrankFactory auf Reflection um und lesen den zu erzeugenden Möbeltyp aus einer Konfigurationsdatei aus. Der Einfachheit halber werden wir die Konfigurationsdatei mocken und nicht mit einer tatsächlich vorhandenen Datei arbeiten.

Im folgenden die angepasste SchrankFactory Klasse

1: public class SchrankFactory {
2:
3:  public static Schrank getSchrank() {
4:	String schrankArt = readFromConfig();
5:	try {
6:		Schrank schrank = (Schrank) Class.forName(schrankArt).newInstance();
7:		return schrank;
8:	} catch (InstantiationException e) {
9:		// TODO Auto-generated catch block
10:		e.printStackTrace();
11:	} catch (IllegalAccessException e) {
12:		// TODO Auto-generated catch block
13:		e.printStackTrace();
14:	} catch (ClassNotFoundException e) {
15:		// TODO Auto-generated catch block
16:		e.printStackTrace();
17:	}
18:	return null;
19:  }
20:	
21:  private static String readFromConfig() {
22:	//Fürs Beispiel gemockt, eigentlich sollte hier jetzt
23:	//ein File aus einer Konfigurationsdatei ausgelesen werden.
24:	return "Kleiderschrank";
25:  }
26:	
27: }

Beginnen wir damit unsere Schummelei zu erklären. In den Zeilen 21 bis 25 haben wir die Methode readFromConfig. Bei einer echten Implementierung sollte hier natürlich auf eine echte Konfigurationsdatei zugegriffen werden. Die Konfigurationsdatei könnte beispielsweise folgenden Inhalt haben.

produzierterSchrank = "Kleiderschrank"

In unserer Schummel-Implementierung tun wir einfach so als hätten wir den Wert Kleiderschrank aus einer Datei ausgelesen und geben diesen in Zeile 24 zurück.

Die eigentliche Action passiert aber sowieso in der Methode getSchrank. In Zeile vier geht es noch gemütlich zur Sache. Hier wird einfach die readFromConfig() Methode aufgerufen, die den Konfigurationsparameter in die String Variable schrankArt schreibt.

Aber in Zeile 6 geschieht Reflection Magic. Hier verwenden wir die Methode forName der Klasse Class, die uns eine Referenz auf die Klasse Kleiderschrank liefert und das nur weil die Variable schrankArt den Wert Kleiderschrank enthält. Wenn in der Variablen der Wert Wohnzimmerschrank gespeichert wäre, dann würden wir eine Referenz auf die Wohnzimmerschrank Klasse erhalten.

Rufen wir die Methode forName allerdings mit einem Wert auf, der nicht in einen Klassennamen aufgelöst werden kann, so wird eine entsprechende Exception geworfen. Das ist auch der Grund dafür, weshalb die Verwendung der Reflection in einem try catch Anweisungsblock stehen muss.

Um eine Instanz der Kleiderschrank Klasse zu erzeugen rufen wir die Methode newInstance auf. Diese Methode liefert uns eine Object Instanz zurück, welche wir noch mit Hilfe eine Castings in eine Kleiderschrank Instanz umwandeln müssen. Und das alles passiert ohne, dass wir, wie üblich das Schlüsselwort new verwenden. Wie cool ist das denn bitte?

Jetzt fehlt nur noch der letzte Schritt. Wir müssen noch das Hauptprogramm anpassen! D.h. den Übergabeparameter der getSchrank Methode entfernen.

public static void main(String[] args) {
  Schrank schrank = SchrankFactory.getSchrank();
  schrank.oeffneSchrank();
}

Starten wir das Programm, so erhalten wir die erwartete Ausgabe.

Öffne einen Kleiderschrank!

Was ist Reflection?

Mit Hilfe von Reflection können wir also Methoden (und auch Attribute) einer Klasse zur Laufzeit über deren Namen aufrufen.

Da Programmcode, der Reflection verwendet, allerdings um ein Vielfaches weniger Performant ist, sollte man dieses Werkzeug nur sehr begrenzt einsetzen. Daher werden wir uns im nächsten Abschnitt noch eine Möglichkeit ansehen, wie wir das Factory Pattern so weit abstrahieren können, dass wir auf den Einsatz von Reflection verzichten können.

Herzlichen Glückwunsch! Du hast das Factory Pattern implementiert!

Das Abstract Factory Pattern

Wir können das Factory Pattern noch weiter abstrahieren.

Nehmen wir an es gäbe jede Schrankart noch zusätzlich in einer Deluxe-Variante. In diesem Fall würden wir noch ein weiteres Interface DeluxeSchrank benötigen.

public interface SchrankDeluxe {
  void oeffneDeluxeSchrank();
}

Wie können wir diesen Fall berücksichtigen? Genau das ist Aufgabe des abstract Factory Pattern.

Um das abstract Factory Pattern zu verwenden führen wir zunächst eine abstrakte Klasse ein, die wir AbstractSchrankFactory nennen, welche zwei abstrakte Methoden enthält, die ein Schrank bzw. ein SchrankDeluxe Objekt zurückliefern.

public abstract class AbstractSchrankFactory {
  public abstract Schrank getSchrank();
  public abstract SchrankDeluxe getSchrankDeluxe();
}

Als nächstes müssen wir für die Implementierung der abstrakten Methoden sorgen. Was wir wie üblich dadurch bewerkstelligen, dass wir entsprechende Unterklassen erstellen, welche jeweils von der AbstractSchrankFactory abgeleitet sind.

Überlegen wir uns welche Klassen wir brauchen.

Zunächst erstellen wir für jeden Schranktyp eine eigene Factory Klasse. Also eine KleiderschrankFactory, KuechenschrankFactory und eine WohnzimmerFactory.

Außerdem müssen wir noch die drei Klassen KleiderschrankDeluxe, KuechenschrankDeluxe und WohnzimmerSchrankDeluxe implementieren, welche jeweils das Interface SchrankDeluxe implementieren.

Wir wollen uns hier nur auf den Fall des Kleiderschranks konzentrieren, da die beiden anderen Fälle völlig analog umzusetzen sind.

Beginnen wir mit der Klasse KleiderschrankDeluxe.

public class KleiderschrankDeluxe implements SchrankDeluxe{

  @Override
  public void oeffneDeluxeSchrank() {
	System.out.println("Kleiderschrank Deluxe");
  }	
}

Die Klasse KleiderschrankDeluxe implementiert lediglich das Interface SchrankDeluxe. In der zu überschreibenden Methode oeffneDeluxeSchrank machen wir es uns wieder leicht und geben einfach eine Meldung auf dem Bildschirm aus.

Als nächstes müssen wir uns um die KleiderschrankFactory kümmern. Diese leiten wir von der AbstractSchrankFactory ab.

public class KleiderschrankFactory extends AbstractSchrankFactory {

  @Override
  public Schrank getSchrank() {
	return new Kleiderschrank();
  }

  @Override
  public SchrankDeluxe getSchrankDeluxe() {
	return new KleiderschrankDeluxe();
  }
}

Diese Klasse implementiert die Methoden getSchrank() und getSchrankDeluxe() aus der AbstractSchrankFactory Klasse.

In der Methode getSchrank wird eine Kleiderschrank und in der Methode getSchrankDeluxe eine KleiderschrankDeluxe Instanz erzeugt und zurückgegeben.

Es bleibt jetzt noch das Hauptprogramm anzupassen.

1: public static void main(String[] args) {
2:   KleiderschrankFactory factory = new KleiderschrankFactory();
3:   factory.getSchrank().oeffneSchrank();
4:   factory.getSchrankDeluxe().oeffneDeluxeSchrank();
5: }

In Zeile zwei erzeugen wir eine KleiderschrankFactory, aus der wir uns in Zeile drei einen Kleiderschrank bzw. in Zeile vier einen deluxe Kleiderschrank holen.

Wenn wir das Programm starten werden, durch Methode oeffneSchrank bzw. oeffneDeluxeSchrank aus den Klassen KleiderschrankDeluxe und Kleiderschrank, folgende Ausgaben erzeugt:

Öffne einen Kleiderschrank!
Kleiderschrank Deluxe

Wir wollen das Vorgehen beim abstract Factory Pattern nochmal in einer Skizze festhalten.

abstract Factory Pattern

Fazit: In diesem Artikel haben wir uns das Factory Pattern angesehen. Das Factory Pattern gehört in die Klasse der creational Design Patterns. Dieses Pattern ermöglicht es uns Objekte zu erzeugen, ohne dass wir uns damit beschäftigen müssen wie diese aufgebaut werden.

Eine Erweiterung des Factory Patterns ist das abstract Factory Pattern, mit dem wir die Verwendung von Reflection vermeiden können und außerdem eine Factory bauen können, die mehr als ein Objekt erzeugt.

Wie die meisten anderen Design Patterns hat auch das Factory Pattern den Zweck Komplexität zu verbergen.

Ich freue mich auf deine Fragen im Kommentarbereich!

Hat dir der Artikel gefallen? Dann folge uns am besten gleich auf Facebook!

Hallo ich bin Kim und ich möchte ein großer Programmierer werden. Machst du mit?

Kommentare (6)

  • Antworte

    Alter , danke , jetzt hab ich albträume.

    • Das tut mir leid! Das war nicht meine Absicht. Ich hoffe du kannst demnächst wieder gut schlafen 🙂 Viele Grüße Kim

  • Antworte

    Hallo Kim,
    vielen Dank für die ganze Zeit und Arbeit, die du in deinen Blog investierst. Diese Artikel erleichtern das Erlernen/ Anwenden der Programmiersprache Java um einiges.

    Ich bin vor kurzem auf deinen Blog gestoßen, da ich mir das Prinzip der objektorientierten Programmierung näher ansehen wollte. Inzwischen habe ich neben diesem Thema noch viele andere Sachen gelernt und verstanden und lese einen Artikel nach dem Anderen 😉.

    Hierbei gefällt mir neben dem Inhalt auch die Art der Informationsübermittlung. Du wiederholst Themen zwar immer wieder kurz, was das Verstehen aller Themen garantiert, jedoch wird die Erklärung nie langweilig. Eingebaute Witze und hervorgehobene Fragen, sowie die gut erläuterten Codestücke erleichtern und verbessern das Lesen noch weiter.

    Ich hoffe du erreichst mit diesem Blog eine größere Leserzahl, welche du auf jeden Fall verdient hättest und führst diese Artikel noch länger fort.

    Viele Grüße
    Pascal

    • Hallo Pascal, vielen Dank für die tolle Rückmeldung. Ich freue mich sehr darüber! Viele Grüße Kim

  • Antworte

    Super Beispiel. Vielleicht ein kleiner Kritikpunkt. Bei Design Pattern sind oft die UML-Diagramme sehr wichtig und hilfreich. Am besten würde ich immer das abstrakte bzw. allgemeine UML-Diagramm des Patterns zeigen, dann auf ein Beispiel eingehen und dann dieses Beispiel nochmal als UML Darstellen. So hätte man meiner Meinung nach den besten Lerneffekt.
    Gruß und schöner Beitrag.

    • Hallo Sebastian, natürlich UML Diagramme veranschaulichen das ganze zusätzlich. Viele Grüße Kim

Antworte auf cocoapoco Abbruch