Zum Inhalt
wannacode

Warum Muster

Kurz gesagt: Ein Entwurfsmuster ist eine benannte, bewährte Lösung für ein wiederkehrendes Problem im Code; die klassischen 23 Muster gehören zu drei Familien: erzeugende, strukturelle und Verhaltensmuster.

Problem

Zwei Absätze für ein einziges Wort

Ein Code-Review in einem Online-Shop. Jonas liest die Kasse und hinterlässt einen Kommentar an der markierten Zeile:

interface PaymentRule {
double total(double amount);
}
class Checkout {
private final Map<String, PaymentRule> rules = Map.of(
"Karte", amount -> amount,
"Nachnahme", amount -> amount + 2.50,
"Gutschein", amount -> Math.max(amount - 10, 0));
public double pay(String method, double amount) {
return rules.get(method).total(amount);
}
}
Checkout checkout = new Checkout();
System.out.printf("%.2f €%n", checkout.pay("Nachnahme", 80));
System.out.printf("%.2f €%n", checkout.pay("Gutschein", 80));
Ausgabe
82.50 €
70.00 €

Jonas: Ich verstehe nicht, was das ist. Die Kasse hält eine Tabelle, in der jede Zahlungsart eine eigene Regel ist. Alle Regeln haben dieselbe Form: Sie nehmen einen Betrag und geben einen Betrag zurück. Während das Programm läuft, nimmt die Kasse die Regel unter dem Namen, den der Kunde gewählt hat, und ruft sie auf, ohne zu wissen, welche es ist. Ist das Absicht? Wenn morgen die Ratenzahlung kommt, muss ich dann pay anfassen?

Maria: Das ist Absicht. Du fasst pay nicht an, du fügst eine Zeile in die Tabelle ein. Die Regel rechnet nur und merkt sich nichts, deshalb kann die Kasse sie mehrmals fragen: für die Vorschau, für die Rechnung, für die Zahlung. Sonst würde jede Frage einen anderen Betrag ergeben…

Zwei Leute, zwei Absätze, und das Wichtigste ist immer noch nicht gesagt: Was passiert, wenn der Name nicht in der Tabelle steht?

Maria hätte ein einziges Wort schreiben können: Strategy. Jonas hätte gewusst, was er vor sich hat und was er prüfen muss. Die Regel rechnet nur. Eine neue Zahlungsart ist eine neue Zeile, und die Kasse wird nicht angefasst. Ein unbekannter Name wird abgefangen, bevor er die Tabelle erreicht. Viele Leute haben diese Fragen schon gelöst, und sie sind unter diesem Namen festgehalten.

Die Idee

Ein Muster ist der Name einer Lösung

Ein Entwurfsmuster ist eine bewährte Lösung für ein Problem, das in vielen Projekten auftaucht, mit einem Namen, den alle kennen. Es ist keine Bibliothek und kein Code zum Kopieren: Jedes Projekt schreibt es auf seine Weise. Ein Muster trägt vier Dinge:

  • einen Namen: ein Wort statt zwei Absätzen;
  • das Problem, das es löst: zum Beispiel „die Regel ändert sich, und die Kasse darf nicht angefasst werden“;
  • die Rollen: wer was hält und wer wen aufruft;
  • die Kosten: was du dafür zahlst (mehr Klassen, ein Sprung mehr beim Lesen) und wann es sich nicht lohnt.

Die meisten Namen stammen aus dem Buch „Design Patterns“ (1994) von vier Autoren, die Entwickler die „Gang of Four“ nennen. Es beschreibt 23 Muster in drei Familien. Das Modul geht die am häufigsten benutzten durch:

Repository und Schichten stammt nicht aus dem Buch. Es ist ein Architekturmuster: wo im Programm die Datenbank lebt.

Das Modul endet mit einer Übung, Der Shop: welches Muster wo: ein unordentlicher Shop, den du selbst aufräumst.

Sieh es dir an

Die Falle: ein Muster um des Musters willen

Die Aufgabe ist eine einzige: 10 % Rabatt für Bestellungen über 100 €. Jemand hat gerade etwas über Muster gelesen:

interface DiscountStrategy {
double apply(double amount);
}
class PercentOverThresholdDiscountStrategy implements DiscountStrategy {
private final double percent;
private final double threshold;
public PercentOverThresholdDiscountStrategy(double percent, double threshold) {
this.percent = percent;
this.threshold = threshold;
}
@Override
public double apply(double amount) {
return amount > threshold ? amount * (1 - percent / 100) : amount;
}
}
abstract class AbstractDiscountStrategyFactory {
public abstract DiscountStrategy createStrategy();
}
class DefaultDiscountStrategyFactory extends AbstractDiscountStrategyFactory {
@Override
public DiscountStrategy createStrategy() {
return new PercentOverThresholdDiscountStrategy(10, 100);
}
}
class DiscountService {
private final AbstractDiscountStrategyFactory factory;
public DiscountService(AbstractDiscountStrategyFactory factory) {
this.factory = factory;
}
public double discounted(double amount) {
return factory.createStrategy().apply(amount);
}
}
DiscountService service = new DiscountService(new DefaultDiscountStrategyFactory());
System.out.printf("Summe: %.2f €%n", service.discounted(120));
System.out.printf("Summe: %.2f €%n", service.discounted(80));
Ausgabe
Summe: 108.00 €
Summe: 80.00 €

Fünf Klassen, zwei Muster (Strategy und eine Fabrik) und ein Name wie AbstractDiscountStrategyFactory. Dieselbe Aufgabe ohne Muster:

static double discounted(double amount) {
return amount > 100 ? amount * 0.9 : amount;
}
System.out.printf("Summe: %.2f €%n", discounted(120));
System.out.printf("Summe: %.2f €%n", discounted(80));
Ausgabe
Summe: 108.00 €
Summe: 80.00 €

Die Ausgabe ist gleich. Der Unterschied liegt im Lesen: Um herauszufinden, wie groß der Rabatt ist, gehst du in der ersten Version durch fünf Klassen, in der zweiten liest du eine Zeile.

Die Muster in der ersten Version lösen überhaupt kein Problem. Es gibt einen einzigen Rabatt, niemand ändert ihn, während das Programm läuft, und niemand wählt zwischen Rabatten. Wenn der zweite Rabatt kommt und der Kunde wählen muss, wird aus der Funktion eine Strategy. Nicht früher.

Probier es aus

Welche Familie, und braucht es ein Muster

1. Builder, Singleton und Factory Method gehören zu einer Familie. Zu welcher?
2. Die Bibliothek des Kurierdienstes will sendParcel(city, grams), und dein Code ruft ship(city, kilograms) auf. Aus welcher Familie ist die Lösung?
3. Das Etikett zeigt den Preis „inkl. MwSt.“ oder „ohne MwSt.“. Das wird an einer einzigen Stelle entschieden, und die beiden Varianten haben sich seit Jahren nicht geändert. Brauchst du ein Muster?
4. Der Kunde wählt eine von fünf Zahlungsarten, und jeden Monat kommt eine neue dazu. Brauchst du ein Muster?

Die Idee

Wann ein Muster schadet

Ein Muster ist die Antwort auf ein Problem. Ohne das Problem bleiben nur die Kosten:

  • „Wir könnten es brauchen.“ Eine Schnittstelle mit einer einzigen Implementierung, eine Fabrik für eine einzige Klasse, eine Strategy mit einer einzigen Variante. Alle, die den Code lesen, zahlen dafür, und niemand benutzt es.
  • Der Name nennt das Muster, nicht die Aufgabe. AbstractDiscountStrategyFactory sagt, wie es gebaut ist, discounted sagt, was es tut. Wenn du die Klasse nicht ohne die Wörter des Musters beschreiben kannst, brauchst du sie wahrscheinlich nicht.
  • Ein Muster aus einer anderen Sprache. Wo ein Lambda genügt, sind eine Schnittstelle und eine Klasse für jede Regel zu viel.
  • Das Muster vor dem Schmerz. Die gute Reihenfolge ist die umgekehrte. Du schreibst das Einfache. Wenn derselbe switch an einer dritten Stelle auftaucht oder die Regeln fünf werden, baust du per Refactoring zum Muster um. Dann weißt du genau, was sich ändert.

Muster lernst du, um sie im Code anderer und in Reviews zu erkennen und zu benennen. Nicht, um sie überall einzubauen.

Übung

Erkenne das Muster

Löse das in deinem Editor.

Drei Ausschnitte aus einem Shop. Führe sie aus, und sage zu jedem: Gibt es ein Muster, welches ist es (Strategy, Observer, Factory Method, Singleton oder Adapter aus der Lektion „Entwurfsmuster“ im Modul Modernes Java), und in einem Satz: Welches Problem löst es. Einer der drei braucht kein Muster.

A.

class Order {
private final List<Consumer<String>> listeners = new ArrayList<>();
public void onShipped(Consumer<String> listener) {
listeners.add(listener);
}
public void ship(String id) {
for (Consumer<String> listener : listeners) {
listener.accept(id);
}
}
}
Order order = new Order();
order.onShipped(id -> System.out.println("E-Mail: Bestellung " + id + " ist versandt"));
order.onShipped(id -> System.out.println("Lager: " + id + " aus der Warteschlange nehmen"));
order.ship("1042");
Ausgabe
E-Mail: Bestellung 1042 ist versandt
Lager: 1042 aus der Warteschlange nehmen

B.

class CourierApi {
public String sendParcel(String city, int grams) {
return "Paket für " + city + ", " + grams + " g";
}
}
interface Shipping {
String ship(String city, double kilograms);
}
class CourierShipping implements Shipping {
private final CourierApi api = new CourierApi();
@Override
public String ship(String city, double kilograms) {
return api.sendParcel(city, (int) Math.round(kilograms * 1000));
}
}
Shipping shipping = new CourierShipping();
System.out.println(shipping.ship("Köln", 1.25));
Ausgabe
Paket für Köln, 1250 g

C.

static String priceLabel(double price, boolean withVat) {
if (withVat) {
return String.format("%.2f € inkl. MwSt.", price * 1.20);
}
return String.format("%.2f € ohne MwSt.", price);
}
System.out.println(priceLabel(25, true));
System.out.println(priceLabel(25, false));
Ausgabe
30.00 € inkl. MwSt.
25.00 € ohne MwSt.
Die Antworten
  • A ist Observer. Die Bestellung weiß nicht, wer ihr zuhört. Die E-Mail und das Lager abonnieren, und ship ruft sie in der Reihenfolge auf, in der sie sich angemeldet haben. Ein neuer Listener fasst Order nicht an.
  • B ist Adapter. CourierShipping übersetzt dein ship (Kilogramm) in das fremde sendParcel (Gramm), ohne eines von beiden anzufassen.
  • C braucht kein Muster. Zwei Varianten, an einer Stelle, niemand ändert sie. Strategy wäre hier eine Schnittstelle und zwei Klassen statt eines einzigen if.

Weitere Aufgaben

  • Wann C ein Muster braucht. Der Shop beginnt, in fünf Ländern mit unterschiedlicher Mehrwertsteuer zu verkaufen, und der Kunde wählt das Land. Was ändert sich? Schreibe auf, welches Muster helfen würde und warum, ohne Code.
  • Das Muster, das verschwindet. Nimm die fünf Klassen aus der Karte „Die Falle“ und ersetze sie durch eine einzige Funktion. Zähle die Zeilen davor und danach. Die Ausgabe muss gleich bleiben.
  • Dein Code. Suche in einem deiner Projekte einen switch oder eine Kette von ifs, die sich an zwei Stellen wiederholt. Mit welchem Wort würdest du ihn in einem Review benennen?

Test

Teste dich

1. Was ist ein Entwurfsmuster?
2. Warum hilft der Name eines Musters in einem Code-Review?
3. Woran erkennst du, dass ein Muster nicht nötig ist?
4. Was ist die gute Reihenfolge?
5. Adapter, Decorator und Composite sind:

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