Zum Inhalt
wannacode

Builder

Kurz gesagt: Builder baut ein komplexes Objekt Schritt für Schritt zusammen, mit einem Namen für jedes Feld, prüft es in build() und gibt ein fertiges unveränderliches Objekt zurück.

Problem

Neun Parameter und zwei vertauschte Wörter

Der Shop schickt eine E-Mail, wenn eine Bestellung versandt wird. Die E-Mail hat neun Felder: an wen sie geht, die Adresse für Antworten, den Betreff, den Text, eine Kopie, eine Blindkopie, einen Anhang, ob sie wichtig ist und ob das Öffnen verfolgt wird.

class Email {
private final String to;
private final String replyTo;
private final String subject;
private final String body;
private final String cc;
private final String bcc;
private final String attachment;
private final boolean highPriority;
private final boolean trackOpens;
public Email(String to, String replyTo, String subject, String body, String cc,
String bcc, String attachment, boolean highPriority, boolean trackOpens) {
this.to = to;
this.replyTo = replyTo;
this.subject = subject;
this.body = body;
this.cc = cc;
this.bcc = bcc;
this.attachment = attachment;
this.highPriority = highPriority;
this.trackOpens = trackOpens;
}
public void send() {
System.out.println("An: " + to);
if (replyTo != null) {
System.out.println("Antwort an: " + replyTo);
}
System.out.println("Betreff: " + subject);
if (attachment != null) {
System.out.println("Anhang: " + attachment);
}
if (highPriority) {
System.out.println("Hohe Priorität");
}
}
}

So wird die Bestätigung für Bestellung 1042 verschickt:

Email email = new Email("support@shop.example", "maria@example.com", "Bestellung 1042 wurde versandt",
"Dein Paket ist unterwegs.", null, null, "invoice-1042.pdf", true, false);
email.send();
Ausgabe
An: support@shop.example
Antwort an: maria@example.com
Betreff: Bestellung 1042 wurde versandt
Anhang: invoice-1042.pdf
Hohe Priorität

Die E-Mail ging an den Support des Shops, und Maria hat nichts bekommen. Die ersten beiden Argumente sind vertauscht. Beide sind Text, also kann der Compiler es nicht erkennen. Und welches ist am Ende true und welches false? Um das herauszufinden, zählst du Positionen im Konstruktor. Zwei null halten die Plätze von Feldern frei, die diese E-Mail nicht hat.

Jedes neue Feld, sagen wir die Sprache der E-Mail, ändert den Konstruktor und jede Stelle, die ihn aufruft.

Die Idee

Builder: über Namen, in Schritten, mit Prüfung

Das Objekt entsteht nicht durch einen Aufruf mit neun Positionen, sondern in Schritten, jeder mit einem Namen: .to(…), .subject(…). Die Schritte gehen an ein eigenes Objekt, den Builder. Er sammelt die Werte, und am Ende build():

  • prüft die Pflichtfelder (hier: Empfänger, Betreff und Text) und wirft eine Exception, wenn eins fehlt;
  • erzeugt ein unveränderliches Objekt: Danach kann sich die E-Mail nicht mehr ändern.

Das ist das Muster Builder. Die Rollen:

  • das Produkt: Email, unveränderlich, mit einem privaten Konstruktor, sodass es keinen anderen Weg gibt, eine zu erzeugen;
  • der Builder: Email.Builder, mit einer Methode für jedes Feld. Die Methode gibt den Builder selbst zurück, sodass sich die Aufrufe in einer Kette aneinanderreihen;
  • build(): die einzige Stelle, an der das Produkt geprüft und erzeugt wird.

Sieh es dir an

Die Kette und build()

class Email {
private final String to;
private final String replyTo;
private final String subject;
private final String body;
private final String cc;
private final String bcc;
private final String attachment;
private final boolean highPriority;
private final boolean trackOpens;
private Email(Builder b) {
to = b.to;
replyTo = b.replyTo;
subject = b.subject;
body = b.body;
cc = b.cc;
bcc = b.bcc;
attachment = b.attachment;
highPriority = b.highPriority;
trackOpens = b.trackOpens;
}
public boolean highPriority() {
return highPriority;
}
public void send() {
System.out.println("An: " + to);
if (replyTo != null) {
System.out.println("Antwort an: " + replyTo);
}
System.out.println("Betreff: " + subject);
if (attachment != null) {
System.out.println("Anhang: " + attachment);
}
if (highPriority) {
System.out.println("Hohe Priorität");
}
}
static class Builder {
private String to;
private String replyTo;
private String subject;
private String body;
private String cc;
private String bcc;
private String attachment;
private boolean highPriority;
private boolean trackOpens;
public Builder to(String value) { to = value; return this; }
public Builder replyTo(String value) { replyTo = value; return this; }
public Builder subject(String value) { subject = value; return this; }
public Builder body(String value) { body = value; return this; }
public Builder cc(String value) { cc = value; return this; }
public Builder bcc(String value) { bcc = value; return this; }
public Builder attachment(String value) { attachment = value; return this; }
public Builder highPriority() { highPriority = true; return this; }
public Builder trackOpens() { trackOpens = true; return this; }
public Email build() {
if (to == null) {
throw new IllegalStateException("Empfänger fehlt");
}
if (subject == null) {
throw new IllegalStateException("Betreff fehlt");
}
if (body == null) {
throw new IllegalStateException("Text fehlt");
}
return new Email(this);
}
}
}

Ein Builder kostet Code: Jedes Feld wird dreimal geschrieben: in der E-Mail, im Builder und in seiner Methode. Deshalb baut man ihn nicht für jede Klasse.

Email.Builder ist eine geschachtelte Klasse: Sie sieht den privaten Konstruktor von Email, sonst niemand. Die Felder von Email sind final.
Email email = new Email.Builder()
.to("maria@example.com")
.subject("Bestellung 1042 wurde versandt")
.body("Dein Paket ist unterwegs.")
.attachment("invoice-1042.pdf")
.highPriority()
.build();
email.send();
Email.Builder draft = new Email.Builder()
.to("peter@example.com")
.body("Dein Paket ist unterwegs.");
try {
draft.build();
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
}
Ausgabe
An: maria@example.com
Betreff: Bestellung 1042 wurde versandt
Anhang: invoice-1042.pdf
Hohe Priorität
Betreff fehlt

Email email = new Email.Builder()
.to("maria@example.com")
.subject("Bestellung 1042 wurde versandt")
.body("Dein Paket ist unterwegs.")
.attachment("invoice-1042.pdf")
.highPriority()
.build();
email.send();
Email.Builder draft = new Email.Builder()
.to("peter@example.com")
.body("Dein Paket ist unterwegs.");
try {
draft.build();
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
}
Schritt für Schritt, in Worten
  1. new Email.Builder(): ein leerer Bauplan mit neun Feldern. Die drei Pflichtfelder (to, subject, body) sind markiert.
  2. .to(…) füllt ein Feld und gibt denselben Builder zurück, also geht die Kette weiter.
  3. Jeder Aufruf hat einen Namen: Ein Wert kann nicht im falschen Feld landen, wie es bei den Positionen im Konstruktor passierte.
  4. .body(…): Alle drei Pflichtfelder sind gefüllt.
  5. Optionale Felder schreibst du nur, wenn du sie brauchst. highPriority() hat kein Argument: kein true mehr an achter Stelle.
  6. build() prüft die Pflichtfelder und stanzt eine Email aus. Ihre Felder sind final: Das Objekt ändert sich nicht mehr.
  7. Die E-Mail geht an Maria, mit der Rechnung, mit hoher Priorität.
  8. Ein neuer Builder, ein neuer leerer Bauplan. Hier hat jemand den Betreff vergessen.
  9. draft.build() stößt auf das leere Feld subject und bricht mit einer Exception ab. Eine E-Mail ohne Betreff entsteht gar nicht erst.
  10. Der Fehler sagt, was fehlt. Es wurde nichts gesendet.

Die E-Mail ohne Betreff erreicht nie die Post: build() stoppt sie schon beim Zusammenbau.

Sieh es dir an

Ohne Builder: was die Sprache bietet

Java hat keine benannten Argumente. Ein record schreibt die Felder, den Konstruktor und die Zugriffsmethoden, und das Objekt ist unveränderlich, aber neun Parameter sind trotzdem neun Positionen. Für die häufigen Fälle gibt es also eine Factory-Methode mit einem Namen (wie in der Lektion Factory), und die Prüfung kommt in den kompakten Konstruktor:
record Email(String to, String replyTo, String subject, String body, String cc,
String bcc, String attachment, boolean highPriority, boolean trackOpens) {
Email {
if (to == null || subject == null || body == null) {
throw new IllegalArgumentException("Empfänger, Betreff oder Text fehlt");
}
}
static Email orderShipped(String to, int orderId, String invoice) {
return new Email(to, "support@shop.example", "Bestellung " + orderId + " wurde versandt",
"Dein Paket ist unterwegs.", null, null, invoice, false, false);
}
}
Email email = Email.orderShipped("maria@example.com", 1042, "invoice-1042.pdf");
System.out.println(email.to() + " / " + email.subject() + " / " + email.attachment());
Ausgabe
maria@example.com / Bestellung 1042 wurde versandt / invoice-1042.pdf
Für alles andere benutzt Java-Code einen Builder. Die Standardbibliothek macht es selbst: HttpRequest.newBuilder().uri(…).header(…).build(). Die Bibliothek Lombok schreibt den Builder mit der Annotation @Builder für dich.

Wann sich ein Builder trotzdem lohnt:

  • die E-Mail wird in Schritten an verschiedenen Stellen des Codes zusammengesetzt: Der Anhang kommt nur dazu, wenn es eine Rechnung gibt, die Kopie nur bei Firmen;
  • es gibt eine Prüfung über mehrere Felder: Zum Beispiel darf die Blindkopie nicht die Adresse des Empfängers sein;
  • es gibt viele Felder, die meisten optional, und die Sprache hat keine benannten Argumente.

Probier es aus

Sag die Ausgabe voraus

Ein Builder, zwei Aufrufe von build(). Was gibt das Programm aus?

Email.Builder builder = new Email.Builder()
.to("maria@example.com")
.subject("Bestellung 1042 wurde versandt")
.body("Dein Paket ist unterwegs.");
Email first = builder.build();
builder.highPriority();
Email second = builder.build();
System.out.println(first.highPriority() + " " + second.highPriority());
Antwort

Probier es aus

Finde den Fehler

Der Shop schickt die E-Mails für versandte Bestellungen in einer Schleife. Nur die erste Bestellung hat eine Rechnung:

List<Order> orders = List.of(
new Order("maria@example.com", 1042, "invoice-1042.pdf"),
new Order("peter@example.com", 1043, null));
Email.Builder builder = new Email.Builder().replyTo("support@shop.example");
for (Order order : orders) {
builder.to(order.email())
.subject("Bestellung " + order.id() + " wurde versandt")
.body("Dein Paket ist unterwegs.");
if (order.invoice() != null) {
builder.attachment(order.invoice());
}
builder.build().send();
}
Ausgabe
An: maria@example.com
Antwort an: support@shop.example
Betreff: Bestellung 1042 wurde versandt
Anhang: invoice-1042.pdf
An: peter@example.com
Antwort an: support@shop.example
Betreff: Bestellung 1043 wurde versandt
Anhang: invoice-1042.pdf

Peter hat die Rechnung von Maria bekommen. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Wann du ihn nicht brauchst

Builder verdoppelt den Code der Klasse. Er lohnt sich nicht, wenn:

  • es zwei oder drei Felder gibt und alle Pflicht sind: Ein einfacher Konstruktor ist klarer;
  • ein Record mit einer Factory-Methode für die häufigen Fälle genügt;
  • das Objekt nichts zu prüfen hat und nicht in Schritten zusammengesetzt wird.

Er lohnt sich, wenn es viele Felder gibt und die meisten optional sind, wenn es eine Prüfung über mehrere gibt oder wenn das Objekt in Schritten an verschiedenen Stellen des Codes zusammengesetzt wird.

Übung

Ein Paket für den Kurier

Löse die Aufgabe in deinem Editor.

Ein Builder für ein Paket. Der Kurier will ein Paket mit Empfänger, Telefon, Adresse und Gewicht (Pflicht) und optional: zerbrechlich und Nachnahme. Schreibe Shipment und einen Builder dafür. build() prüft, dass die Pflichtfelder gesetzt sind und dass das Gewicht über 0 und bis 30 kg liegt. Erzeuge ein Paket für Maria (2,5 kg, zerbrechlich, Nachnahme 42,90 €), gib es aus und probiere ein Paket mit 45 kg:

Ausgabe
Paket für Maria, 0888 123 456
Adresse: Gartenstraße 5, Hamburg
Gewicht: 2.5 kg, zerbrechlich
Nachnahme: 42.90 €
Das Gewicht muss über 0 und bis 30 kg liegen

Weitere Aufgaben

  • Die Schleife reparieren. Repariere das Programm aus „Finde den Fehler“ so, dass jede E-Mail nur ihre eigene Rechnung trägt. Die Antwortadresse muss für alle E-Mails bleiben.
  • Eine Kopie mit einer Änderung. Füge eine Methode hinzu, die aus einer fertigen E-Mail einen Builder mit denselben Werten macht (in Java heißt sie oft toBuilder()). Schicke dieselbe E-Mail an einen anderen Empfänger.
  • Der Weg der Sprache. Schreibe Shipment wie in der Karte „Ohne Builder“ und vergleiche die beiden Versionen. Welche ist kürzer, und was verlierst du?

Test

Teste dich

1. new Email("support@…", "maria@…", …) kompiliert, und die E-Mail geht woandershin. Warum hat der Compiler nicht geholfen?
2. Was macht build()?
3. Der Builder wird vor der Schleife erzeugt und für jede E-Mail benutzt. Die zweite E-Mail hat den Anhang der ersten bekommen. Warum?
4. Wann lohnt sich Builder trotzdem?
5. Warum ist Builder in Java häufiger als in C# und Python?

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