Zum Inhalt
wannacode

Factory Method und Simple Factory

Kurz gesagt: Eine Factory hält die Entscheidung, welche Klasse erzeugt wird, an einer Stelle: Simple Factory legt die Wahl an einen Ort, und Factory Method lässt eine Unterklasse entscheiden, welches Objekt entsteht.

Problem

Drei Stellen kennen jede Lieferart

Die Bestellung sagt, wie sie zum Kunden kommt: "address" (an die Haustür), "office" (in einer Kurierfiliale) oder "locker" (in einer Packstation). Jede Art hat eine Klasse mit eigenem Preis und eigenem Etikett:

interface Delivery {
double price();
String label(String recipient);
}
class AddressDelivery implements Delivery {
@Override
public double price() {
return 5.90;
}
@Override
public String label(String recipient) {
return recipient + ", an die Haustür";
}
}
class OfficeDelivery implements Delivery {
@Override
public double price() {
return 3.90;
}
@Override
public String label(String recipient) {
return recipient + ", in einer Kurierfiliale";
}
}
class LockerDelivery implements Delivery {
@Override
public double price() {
return 2.90;
}
@Override
public String label(String recipient) {
return recipient + ", in einer Packstation";
}
}

Drei Teile des Shops brauchen die Lieferung: die Kasse, die Preisvorschau und der Etikettendrucker. Jeder entscheidet selbst, welche Klasse er erzeugt:

static double checkout(String type, double items) {
Delivery delivery = switch (type) {
case "address" -> new AddressDelivery();
case "office" -> new OfficeDelivery();
case "locker" -> new LockerDelivery();
default -> throw new IllegalArgumentException(type);
};
return items + delivery.price();
}
static double previewPrice(String type) {
Delivery delivery = switch (type) {
case "address" -> new AddressDelivery();
case "office" -> new OfficeDelivery();
case "locker" -> new LockerDelivery();
default -> throw new IllegalArgumentException(type);
};
return delivery.price();
}
static String printLabel(String type, String recipient) {
Delivery delivery = switch (type) {
case "office" -> new OfficeDelivery();
default -> new AddressDelivery();
};
return delivery.label(recipient);
}
System.out.printf("Bestellung: %.2f €%n", checkout("locker", 40));
System.out.printf("Vorschau: Lieferung %.2f €%n", previewPrice("locker"));
System.out.println("Etikett: " + printLabel("locker", "Maria"));
Ausgabe
Bestellung: 42.90 €
Vorschau: Lieferung 2.90 €
Etikett: Maria, an die Haustür

Letzten Monat kamen die Packstationen dazu. Jemand hat "locker" in der Kasse und in der Vorschau ergänzt. Der Etikettendrucker wurde davor geschrieben und hat für die Adresse ein default. Maria hat die Lieferung an eine Packstation bezahlt, und der Kurier ist zu ihrer Adresse gefahren.

Eine neue Art sind drei Änderungen, und eine wird vergessen. Die Wahl „welche Klasse für welche Art“ ist kopiert, und die Kopien wissen nichts voneinander.

Die Idee

Simple Factory: new lebt an einer Stelle

Die Wahl „welche Klasse für welche Art“ ist eine einzige Entscheidung, also lebt sie an einer Stelle: in einer Factory. Sie bekommt die Art und gibt ein neues Objekt zurück. Der Aufrufer schreibt kein new und kennt die Klasse nicht: Er bekommt das Interface.

Die Rollen:

  • das Produkt: die gemeinsame Form, das Interface Delivery;
  • die konkreten Produkte: AddressDelivery, OfficeDelivery, LockerDelivery;
  • die Factory: Deliveries.create(type), die einzige Stelle, die weiß, welche Klasse zu welcher Art gehört;
  • die Clients: die Kasse, die Vorschau, das Etikett. Sie fragen nur.

Das heißt Simple Factory. Es stammt nicht aus dem Buch der Gang of Four, ist aber die häufigste Form. Factory Method aus dem Buch ist etwas anderes und kommt weiter hinten in der Lektion.

Eine Regel: Eine unbekannte Art ist ein Fehler, kein stiller Standardwert. Eine Factory, die die Art nicht kennt, wirft eine Exception mit einer klaren Meldung. Sonst verschwindet der Fehler nicht, er fährt mit dem Kurier zur falschen Adresse.

Die Tabelle „Name → Strategie“ aus der Lektion zu Strategy ist dasselbe: eine Factory, die dir zu einem Stück Text ein Objekt gibt.

Sieh es dir an

Eine Factory für die drei Stellen

class Deliveries {
public static Delivery create(String type) {
return switch (type) {
case "address" -> new AddressDelivery();
case "office" -> new OfficeDelivery();
case "locker" -> new LockerDelivery();
default -> throw new IllegalArgumentException("Unbekannte Lieferart: " + type);
};
}
}

Die Kasse, die Vorschau und das Etikett wählen nicht mehr, sie fragen:

static double checkout(String type, double items) {
return items + Deliveries.create(type).price();
}
static double previewPrice(String type) {
return Deliveries.create(type).price();
}
static String printLabel(String type, String recipient) {
return Deliveries.create(type).label(recipient);
}
System.out.printf("Bestellung: %.2f €%n", checkout("locker", 40));
System.out.printf("Vorschau: Lieferung %.2f €%n", previewPrice("locker"));
System.out.println("Etikett: " + printLabel("locker", "Maria"));
Ausgabe
Bestellung: 42.90 €
Vorschau: Lieferung 2.90 €
Etikett: Maria, in einer Packstation

Das Etikett ist für eine Packstation. Die Wahl steht an einer Stelle, also kann sie nicht an der Kasse stimmen und auf dem Etikett falsch sein.

Genauer, für Neugierige

Die Factory kann auch eine Tabelle sein: Map<String, Supplier<Delivery>> mit Werten wie AddressDelivery::new. Eine Tabelle ist praktisch, wenn die Arten von außen hinzukommen (jedes Modul registriert seine eigene). Ein switch ist klarer, wenn alle Arten an einer Stelle bekannt sind, und der Compiler prüft, dass es ein default gibt (siehe die Karte „Unbekannte Art“).

Sieh es dir an

Eine neue Art: eine Klasse und eine Zeile

Der Shop führt die Abholung im Geschäft ein: Art "pickup", kostenlos. Neu ist eine Klasse:

class PickupDelivery implements Delivery {
@Override
public double price() {
return 0.00;
}
@Override
public String label(String recipient) {
return recipient + ", im Geschäft";
}
}

und eine Zeile in der Factory, hier hervorgehoben:

class Deliveries {
public static Delivery create(String type) {
return switch (type) {
case "address" -> new AddressDelivery();
case "office" -> new OfficeDelivery();
case "locker" -> new LockerDelivery();
case "pickup" -> new PickupDelivery();
default -> throw new IllegalArgumentException("Unbekannte Lieferart: " + type);
};
}
}
Delivery home = Deliveries.create("address");
System.out.printf("%s: %.2f €%n", home.label("Maria"), home.price());
Delivery locker = Deliveries.create("locker");
System.out.printf("%s: %.2f €%n", locker.label("Maria"), locker.price());
Delivery pickup = Deliveries.create("pickup");
System.out.printf("%s: %.2f €%n", pickup.label("Maria"), pickup.price());
Ausgabe
Maria, an die Haustür: 5.90 €
Maria, in einer Packstation: 2.90 €
Maria, im Geschäft: 0.00 €

Delivery home = Deliveries.create("address");
System.out.printf("%s: %.2f €%n", home.label("Maria"), home.price());
Delivery locker = Deliveries.create("locker");
System.out.printf("%s: %.2f €%n", locker.label("Maria"), locker.price());
Delivery pickup = Deliveries.create("pickup");
System.out.printf("%s: %.2f €%n", pickup.label("Maria"), pickup.price());
Schritt für Schritt, in Worten
  1. Deliveries.create("address") bekommt nur den Text aus der Bestellung. Die Factory sucht die passende Form dafür.
  2. Aus der Form kommt ein neues AddressDelivery, aber home hat den Typ Delivery: Der Aufrufer sieht nur price() und label().
  3. Dieselbe Factory, ein anderer Text: „locker“ geht an den anderen Formen vorbei und hält bei LockerDelivery.
  4. Das Etikett sagt jetzt Packstation: Die Klasse wird an einer Stelle gewählt, und die Kasse, die Vorschau und das Etikett fragen alle diese Stelle.
  5. Eine neue Art: die Klasse PickupDelivery und eine Zeile mehr in der Factory. Die Zeilen, die Deliveries.create aufrufen, ändern sich nicht.
  6. „pickup“ läuft durch wie jede andere Art. Die neue Art wurde an einer Stelle hinzugefügt.

Die Kasse, die Vorschau und das Etikett werden nicht angefasst. Sie erfahren von der neuen Art, wenn eine Bestellung sie verlangt.

Sieh es dir an

Unbekannte Art

Die Art kommt von außen: aus einem Formular, von der Seite eines Partners, aus einer alten Datei. Früher oder später taucht eine Art auf, die es nicht gibt:

try {
Delivery delivery = Deliveries.create("drone");
System.out.println(delivery.label("Maria"));
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
Ausgabe
Unbekannte Lieferart: drone

Der Fehler sagt, welche Art unbekannt ist. Er wird dort abgefangen, wo die Bestellung in den Shop kommt, und der Kunde sieht eine Meldung statt eines Pakets am falschen Ort.

Und wenn die Factory kein default hat? Java lässt dich das nicht vergessen: Ein switch-Ausdruck muss jeden Wert abdecken, sonst lässt sich das Programm nicht kompilieren:
class Deliveries {
public static Delivery create(String type) {
return switch (type) {
case "address" -> new AddressDelivery();
case "office" -> new OfficeDelivery();
case "locker" -> new LockerDelivery();
};
}
}
Fehler
error: the switch expression does not cover all possible input values

Sieh es dir an

Factory Method: die Unterklasse entscheidet

Simple Factory wählt nach Text. Manchmal richtet sich die Wahl aber nicht nach Text, sondern danach, welches Objekt fragt.

Der Verkaufsbericht wird als CSV (für die Buchhaltung) und als HTML-Tabelle (in einer E-Mail) verschickt. Beide tun dasselbe: Jedes Produkt ist eine Zeile, und am Ende steht eine Zeile „Gesamt“. Nur die Datei, in die die Zeilen geschrieben werden, ist anders:

interface ReportFile {
void add(String product, double total);
String text();
}
class CsvFile implements ReportFile {
private final List<String> lines = new ArrayList<>(List.of("Produkt;Summe"));
@Override
public void add(String product, double total) {
lines.add(product + ";" + String.format("%.2f", total));
}
@Override
public String text() {
return String.join("\n", lines);
}
}
class HtmlFile implements ReportFile {
private final List<String> rows = new ArrayList<>();
@Override
public void add(String product, double total) {
rows.add("<tr><td>" + product + "</td><td>" + String.format("%.2f", total) + "</td></tr>");
}
@Override
public String text() {
return "<table>\n" + String.join("\n", rows) + "\n</table>";
}
}
Der gemeinsame Algorithmus steht in der Basisklasse. Der Schritt „eine Datei erzeugen“ ist eine abstrakte Methode, die jede Unterklasse liefert:
record Sale(String product, double total) {}
abstract class ReportExporter {
protected abstract ReportFile createFile();
public String export(List<Sale> sales) {
ReportFile file = createFile();
double overall = 0;
for (Sale sale : sales) {
file.add(sale.product(), sale.total());
overall += sale.total();
}
file.add("Gesamt", overall);
return file.text();
}
}
class CsvExporter extends ReportExporter {
@Override
protected ReportFile createFile() {
return new CsvFile();
}
}
class HtmlExporter extends ReportExporter {
@Override
protected ReportFile createFile() {
return new HtmlFile();
}
}
List<Sale> sales = List.of(new Sale("Kaffee", 12.50), new Sale("Tee", 8.00));
System.out.println(new CsvExporter().export(sales));
System.out.println(new HtmlExporter().export(sales));
Ausgabe
Produkt;Summe
Kaffee;12.50
Tee;8.00
Gesamt;20.50
<table>
<tr><td>Kaffee</td><td>12.50</td></tr>
<tr><td>Tee</td><td>8.00</td></tr>
<tr><td>Gesamt</td><td>20.50</td></tr>
</table>

Das ist Factory Method: Die Basisklasse ruft eine Methode auf, die das Objekt erzeugt (createFile()), und die Unterklasse entscheidet, welche Klasse genau.

Warum nicht eine fertige Datei hineingeben? Die Datei sammelt Zeilen. Jeder export muss mit einer leeren Datei beginnen, also hält der Bericht keine Datei, er weiß, wie man eine neue erzeugt.

Wann genügt Simple Factory? Factory Method lohnt sich, wenn es um die Erzeugung herum einen gemeinsamen Algorithmus gibt, den die Unterklassen nicht wiederholen sollen (hier: die Zeilen und die Summe). Willst du nur ein Objekt nach Art, ohne Algorithmus drumherum, ist Simple Factory klarer. Und eine Unterklasse, die nur existiert, um ein neues Objekt zurückzugeben, ist Zeremonie. Dann gib den Konstruktor als Supplier<ReportFile> mit: new ReportExporter(CsvFile::new).

Probier es aus

Was kommt heraus?

Die Arten kommen aus einem Formular auf der Seite. Was gibt das Programm aus?

String[] types = { "office", "Locker", "locker" };
for (String type : types) {
try {
System.out.println(Deliveries.create(type).label("Maria"));
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
Antwort

Probier es aus

Finde den Fehler

Ein anderer Entwickler hat die Factory so geschrieben:

class Deliveries {
public static Delivery create(String type) {
return switch (type) {
case "office" -> new OfficeDelivery();
case "locker" -> new LockerDelivery();
default -> new AddressDelivery();
};
}
}

Die Bestellung kommt von der Seite eines Partners, die die Art mit großem Anfangsbuchstaben sendet:

Delivery delivery = Deliveries.create("Locker");
System.out.println("Etikett: " + delivery.label("Maria"));
System.out.printf("Lieferung: %.2f €%n", delivery.price());
Ausgabe
Etikett: Maria, an die Haustür
Lieferung: 5.90 €

Maria hat eine Packstation gewählt. Sie hat 5,90 € statt 2,90 € für die Lieferung bezahlt, und das Paket ging an ihre Adresse. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Wann du es nicht brauchst

Eine Factory ist ein Sprung mehr beim Lesen: Um zu sehen, welche Klasse erzeugt wird, gehst du woandershin. Es lohnt sich nicht, wenn:

  • diese Stelle immer dieselbe Klasse braucht. new OfficeDelivery() dort, wo die Lieferung immer an eine Filiale geht, ist klarer als eine Factory;
  • die Art beim Schreiben des Codes bekannt ist und nicht als Daten hereinkommt (aus einer Bestellung, einer Datei, einem Formular);
  • es eine Factory für die Factory gibt. DeliveryFactoryFactory bedeutet fast immer, dass eine der beiden überflüssig ist;
  • eine Factory Method eine Unterklasse „für die Zukunft“ hat.

Es lohnt sich, wenn die Art als Daten hereinkommt und die Wahl der Klasse sonst an mehreren Stellen kopiert würde. Oder wenn es um die Erzeugung herum einen gemeinsamen Algorithmus gibt und nur der Schritt „welches Objekt“ anders ist.

Übung

Expresslieferung

Löse das in deinem Editor.

Expresslieferung. Der Shop führt die Expresslieferung an die Haustür ein: Art "express", 9,90 €, Etikett „…, Express an die Haustür“. Füge eine Klasse ExpressDelivery hinzu und mach sie über die Factory verfügbar. Die Kasse, die Vorschau und das Etikett werden nicht angefasst. Erzeuge eine Expresslieferung für Maria und gib sie aus wie in der Karte „Eine neue Art“:

Ausgabe
Maria, Express an die Haustür: 9.90 €

Weitere Aufgaben

  • Großbuchstaben und Leerzeichen. Das Formular sendet "Locker" und " locker ". Lass die Factory sie akzeptieren und ändere nur die Factory. Unbekannte Arten müssen weiter einen Fehler ergeben.
  • Ein JSON-Bericht. Füge dem Bericht aus der Karte „Factory Method“ ein JSON-Format hinzu: [{"product": "Kaffee", "total": 12.50}, …]. Welche Klassen schreibst du, und welche fasst du nicht an?
  • Preis nach Gewicht. Packstationen nehmen Pakete bis 20 kg, und über 5 kg kostet die Lieferung an die Haustür mehr. Wohin gehört das Gewicht: in die Factory oder in die Preismethode? Überlege, warum.

Test

Teste dich

1. Der Shop führt Drohnenlieferung ein, Art "drone". Was schreibst du mit Simple Factory?
2. Die Factory bekommt eine Art, die sie nicht kennt. Was soll sie tun?
3. Wann ist Factory Method besser als Simple Factory?
4. Um Speicher zu sparen, hält die Factory für jede Art ein Objekt und gibt es bei jedem create zurück. Das Objekt hält den Empfänger in einem Feld. Was passiert bei zwei Packstation-Bestellungen hintereinander?
5. Wann brauchst du keine Factory?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·