Der Shop: welches Muster wohin
Kurz gesagt: Eine Übung zu Entwurfsmustern: Ein Shop mit Bezahlung, Benachrichtigungen und Speicherung in einer Methode wird mit Strategy, Observer und Repository neu geordnet, und seine Ausgabe bleibt gleich.
Problem
Ein Shop in einer Methode
Hier ist ein kleiner Onlineshop. Die Bestellmethode berechnet den Betrag aus der Zahlungsart, speichert die Bestellung, schickt eine E-Mail und schreibt ins Lager:
class Shop { private final List<String> orders = new ArrayList<>();
void placeOrder(String id, String customer, double amount, String method) { double total; if (method.equals("Karte")) { total = amount; } else if (method.equals("Nachnahme")) { total = amount + 2.50; } else { throw new IllegalArgumentException("Unbekannte Zahlungsart: " + method); } orders.add(id + ";" + customer + ";" + total); System.out.printf("E-Mail an %s: Bestellung %s, %.2f €%n", customer, id, total); System.out.println("Lager: bereite vor " + id); }
int count() { return orders.size(); }}Shop shop = new Shop();shop.placeOrder("A-1", "ana@example.com", 80, "Karte");shop.placeOrder("A-2", "ivan@example.com", 45, "Nachnahme");System.out.println("Bestellungen: " + shop.count());E-Mail an ana@example.com: Bestellung A-1, 80.00 €Lager: bereite vor A-1E-Mail an ivan@example.com: Bestellung A-2, 47.50 €Lager: bereite vor A-2Bestellungen: 2Es funktioniert. Aber nächsten Monat kommen drei Änderungen: Überweisung mit 2 % Rabatt, eine SMS nach jeder Bestellung und eine Datenbank statt einer Liste. Alle drei stecken in derselben Methode. Jede fasst sie an, und einen Test für den Betrag, ohne dass eine E-Mail rausgeht, kann man nicht schreiben.
In dieser Lektion gibt es kein neues Muster. Sie ist eine Übung: Du erkennst, welches der Muster im Modul jedes dieser Dinge ordnet und welche hier nicht nötig sind.
Die Idee
Das Symptom nennt das Muster
Du suchst dir ein Muster nicht aus einer Liste aus. Du suchst es nach dem, was wehtut:
| Symptom im Code | Muster |
|---|---|
eine Kette von if auf eine Art, die mit jeder neuen Variante wächst |
Strategy, oder Factory, wenn die Kette auswählt, was erzeugt wird |
| nach einem Ereignis ruft der Code selbst alle an, die es erfahren müssen | Observer |
| die Speicherung sitzt in den Regeln, und ein Test ohne Datenbank ist unmöglich | Repository und das Hineinreichen von außen |
ein switch auf den Zustandstext in jeder Methode |
State |
| ein Konstruktor mit vielen optionalen Parametern | Builder |
| eine fremde Bibliothek mit sperriger Schnittstelle | Adapter |
| fünf Teilsysteme, die in genauer Reihenfolge aufgerufen werden müssen | Facade |
| Verhalten „um“ eine Methode: Logging, Cache, Aufschläge auf den Preis | Decorator |
| die letzte Aktion rückgängig machen | Command |
| ein Baum, in dem ein Teil und das Ganze gleich benutzt werden | Composite |
Wenn du das Symptom nicht findest, brauchst du höchstwahrscheinlich kein Muster.
Probier es aus
Erkenne es
Sieh dir den Shop von der ersten Karte noch einmal an.
Sieh es dir an
Eine mögliche Art, ihn zu ordnen
Drei der Symptome stecken im Shop, also sind es auch drei Muster.
Die Zahlungsarten sind Strategies. Das Interface hat eine Methode, also ist jede Art ein Lambda in derMap-Tabelle:interface PaymentMethod { double total(double amount);}record Order(String id, String customer, double total) {}
interface OrderRepository { void save(Order order); int count();}
class InMemoryOrders implements OrderRepository { private final List<Order> orders = new ArrayList<>();
public void save(Order order) { orders.add(order); }
public int count() { return orders.size(); }}
interface OrderListener { void placed(Order order);}class Shop { private final OrderRepository orders; private final Map<String, PaymentMethod> methods; private final List<OrderListener> listeners = new ArrayList<>();
Shop(OrderRepository orders, Map<String, PaymentMethod> methods) { this.orders = orders; this.methods = methods; }
void subscribe(OrderListener listener) { listeners.add(listener); }
void placeOrder(String id, String customer, double amount, String method) { PaymentMethod payment = methods.get(method); if (payment == null) { throw new IllegalArgumentException("Unbekannte Zahlungsart: " + method); } Order order = new Order(id, customer, payment.total(amount)); orders.save(order); for (OrderListener listener : listeners) { listener.placed(order); } }
int count() { return orders.count(); }}Welche Speicherung, welche Arten und welche Abonnenten, wird an einer Stelle entschieden, dort, wo der Shop zusammengesetzt wird:
Map<String, PaymentMethod> methods = Map.of( "Karte", amount -> amount, "Nachnahme", amount -> amount + 2.50);Shop shop = new Shop(new InMemoryOrders(), methods);shop.subscribe(o -> System.out.printf("E-Mail an %s: Bestellung %s, %.2f €%n", o.customer(), o.id(), o.total()));shop.subscribe(o -> System.out.println("Lager: bereite vor " + o.id()));
shop.placeOrder("A-1", "ana@example.com", 80, "Karte");shop.placeOrder("A-2", "ivan@example.com", 45, "Nachnahme");System.out.println("Bestellungen: " + shop.count());E-Mail an ana@example.com: Bestellung A-1, 80.00 €Lager: bereite vor A-1E-Mail an ivan@example.com: Bestellung A-2, 47.50 €Lager: bereite vor A-2Bestellungen: 2Die Ausgabe ist dieselbe, Zeile für Zeile. Das Ordnen hat das Verhalten nicht geändert, und das prüfst du als Erstes. Hier laufen beide Programme bei jeder Prüfung der Seite und geben dieselbe Ausgabe.
Sieh es dir an
Die drei Änderungen jetzt
Überweisung ist eine neue Zeile in der Tabelle. SMS ist ein neuer Abonnent. Die Datenbank wird eine neue Implementierung von OrderRepository. Keine einzige Zeile der Shop-Klasse ändert sich:
Map<String, PaymentMethod> methods = Map.of( "Karte", amount -> amount, "Nachnahme", amount -> amount + 2.50, "Überweisung", amount -> amount * 0.98);Shop shop = new Shop(new InMemoryOrders(), methods);shop.subscribe(o -> System.out.println("SMS an " + o.customer() + ": " + o.id()));
shop.placeOrder("A-3", "ana@example.com", 100, "Überweisung");SMS an ana@example.com: A-3Jetzt ist auch ein Test für den Betrag leicht: Du reichst eine Speicherung im Speicher hinein und abonnierst die E-Mail nicht.
Die Idee
Wo du aufhörst
Das Modul hat mehr als zehn Muster, und drei werden hier benutzt. Die anderen sind nicht vergessen, sie helfen hier nur nicht:
- Builder: Die Bestellung hat drei Pflichtfelder. Der Konstruktor (Record) reicht;
- Singleton: Nichts muss das Einzige sein. Die Speicherung wird von außen hineingereicht, also können der Test und der Shop jeweils ihre eigene haben;
- Factory: Die Zahlungsarten sind Funktionen in einer Tabelle, und die Tabelle ist schon der Ort, an dem die Wahl getroffen wird;
- State: Die Bestellung hat noch keine Zustände. Wenn „bezahlt“ und „versandt“ kommen, kommt es auch (siehe die Aufgaben).
Du hörst auf, wenn die Änderung, die ansteht, leicht wird. Mehr Muster heißt nicht besserer Code.
Übung
Ordne den Shop weiter
Löse die Aufgaben in deinem Editor, ausgehend vom geordneten Shop. Führe nach jedem Schritt das Programm von der Karte „Eine mögliche Art, ihn zu ordnen“ aus: Seine Ausgabe muss gleich bleiben.
1. Die Zustände der Bestellung. Die Bestellung geht neu → bezahlt → versandt und kann storniert werden, aber nicht nach dem Versand. Mach es mit State (oder mit einem Enum und einer Übergangstabelle, wenn die Zustände kein eigenes Verhalten haben) und zeige den Fehler beim Stornieren einer versandten Bestellung.
2. Ein Rabatt über 100 €. Eine Bestellung über 100 € bekommt 5 % Rabatt, egal wie bezahlt wird. Überlege, wohin er gehört: in eine Strategy, in einen Decorator um die Strategy oder in den Shop. Warum?
3. Eine Speicherung, die sich erinnert. Mach eine zweite Speicherung, die die Bestellungen in eine Datei schreibt (eine pro Zeile), und reiche sie dem Shop statt der im Speicher. Die Shop-Klasse ändert sich nicht.
4. Ein fremder Kurierdienst. Das Lager wird jetzt über die Bibliothek eines Kurierdienstes mit der Methode createShipment(reference, weightGrams) aufgerufen. Der Lager-Abonnent wird ein Adapter dazu.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Decorator · Command · State · Repository
- Aufgaben zum Thema: Entwurfsmuster