Strategy
Kurz gesagt: Strategy legt jede Regel (etwa eine Zahlungsart) in ein eigenes Objekt hinter einem gemeinsamen Interface, damit du neue Regeln hinzufügst, ohne den Code anzufassen, der sie benutzt.
Problem
Der Checkout, der jede Zahlungsart kennt
Der Onlineshop nimmt fünf Zahlungsarten an. Nachnahme kostet 2,50 € mehr, ein Gutschein nimmt 10 € ab, bei einer Überweisung gibt es 2 % Rabatt, und Ratenzahlung kostet 5 % mehr. Der Checkout berechnet den Betrag so:
static double total(String method, double amount) { if (method.equals("Karte")) { return amount; } else if (method.equals("Nachnahme")) { return amount + 2.50; } else if (method.equals("Gutschein")) { return Math.max(amount - 10, 0); } else if (method.equals("Überweisung")) { return amount * 0.98; } else if (method.equals("Ratenzahlung")) { return amount * 1.05; } throw new IllegalArgumentException("Unbekannte Zahlungsart: " + method);}System.out.printf("Karte: %.2f €%n", total("Karte", 80));System.out.printf("Nachnahme: %.2f €%n", total("Nachnahme", 80));System.out.printf("Gutschein: %.2f €%n", total("Gutschein", 80));System.out.printf("Überweisung: %.2f €%n", total("Überweisung", 80));System.out.printf("Ratenzahlung: %.2f €%n", total("Ratenzahlung", 80));Karte: 80.00 €Nachnahme: 82.50 €Gutschein: 70.00 €Überweisung: 78.40 €Ratenzahlung: 84.00 €Es funktioniert. Aber jeden Monat kommt eine neue Art, und jede ist ein neuer Zweig in dieser Methode. Dieselbe Kette steckt auch in der Rechnung und in der Bestätigungs-E-Mail. Jemand fügt Apple Pay an zwei der drei Stellen hinzu, und der Kunde bekommt eine E-Mail mit „Unbekannte Zahlungsart“.
Gutscheine gelten nicht mehr nur für 10 €. Um den Wert hineinzureichen, bekommt die Methode einen dritten Parameter, und jede Stelle, die sie aufruft, ändert sich, sogar bei Kartenzahlungen, wo es keinen Gutschein gibt. Der Code, durch den jede Zahlung geht, wird bei jeder Änderung angefasst.
Die Idee
Strategy: die Regel für sich, der Checkout fragt
Was sich ändert, ist eins: wie der Betrag berechnet wird. Was sich nicht ändert, ist der Checkout: Er nimmt den Betrag, fragt und rundet.
Also wandert die Regel in ein eigenes Objekt. Alle Arten haben eine gemeinsame Form: ein Interface mit einer Methode, die einen Betrag nimmt und zurückgibt, wie viel zu zahlen ist. Der Checkout hält einen Verweis auf ein solches Objekt und ruft die Methode auf, ohne zu wissen, welches Objekt es ist.Das ist das Strategy-Muster. Es hat drei Rollen:
- der Checkout hält die Strategy und ruft sie auf. In Büchern heißt er der Kontext;
- die gemeinsame Form: das Interface
PaymentMethod(IPaymentMethodin C#); - die konkreten Strategies: eine Klasse für jede Art.
Eine neue Art ist eine neue Strategy. Der Checkout wird nicht angefasst.
Sieh es dir an
Der Checkout und drei Zahlungsarten
Die gemeinsame Form:
interface PaymentMethod { double total(double amount);}class Card implements PaymentMethod { @Override public double total(double amount) { return amount; }}
class CashOnDelivery implements PaymentMethod { @Override public double total(double amount) { return amount + 2.50; }}
class Voucher implements PaymentMethod { private double value;
public Voucher(double value) { this.value = value; }
@Override public double total(double amount) { return Math.max(amount - value, 0); }}setMethod wechselt sie:class Checkout { private PaymentMethod method;
public Checkout(PaymentMethod method) { this.method = method; }
public void setMethod(PaymentMethod method) { this.method = method; }
public double total(double amount) { return Math.round(method.total(amount) * 100) / 100.0; }}Checkout checkout = new Checkout(new Card());System.out.printf("Karte: %.2f €%n", checkout.total(80));
checkout.setMethod(new CashOnDelivery());System.out.printf("Nachnahme: %.2f €%n", checkout.total(80));
checkout.setMethod(new Voucher(10));System.out.printf("Gutschein: %.2f €%n", checkout.total(80));Karte: 80.00 €Nachnahme: 82.50 €Gutschein: 70.00 €Checkout checkout = new Checkout(new Card());System.out.printf("Karte: %.2f €%n", checkout.total(80));
checkout.setMethod(new CashOnDelivery());System.out.printf("Nachnahme: %.2f €%n", checkout.total(80));
checkout.setMethod(new Voucher(10));System.out.printf("Gutschein: %.2f €%n", checkout.total(80));Schritt für Schritt, in Worten
- Der Checkout bekommt eine Karte: sein Feld method zeigt auf das Objekt Card.
- total(80): Der Checkout kennt die Zahlungsart nicht. Er fragt die Strategy nur nach dem Betrag und bekommt 80.00 zurück.
- Eine neue Art. setMethod stellt das Feld auf das Objekt CashOnDelivery um. In Checkout ändert sich keine einzige Zeile.
- Dasselbe total(80), eine andere Antwort: 82.50.
- Ein Gutschein über 10: ein Objekt mit einem eigenen Feld value. Auf die alten Strategies zeigt nichts mehr.
- Der Gutschein nimmt 10 ab: 70.00. Drei Zahlungsarten, und der Checkout ist die ganze Zeit dasselbe Objekt mit demselben Code.
Der Checkout ist ein Objekt mit einem Stück Code für das ganze Programm. Nur das, worauf das Feld zeigt, ändert sich.
Sieh es dir an
Eine neue Art, ohne den Checkout anzufassen
Überweisung und Ratenzahlung kommen als zwei neue Klassen:
class BankTransfer implements PaymentMethod { @Override public double total(double amount) { return amount * 0.98; }}
class Installments implements PaymentMethod { @Override public double total(double amount) { return amount * 1.05; }}Checkout checkout = new Checkout(new BankTransfer());System.out.printf("Überweisung: %.2f €%n", checkout.total(80));
checkout.setMethod(new Installments());System.out.printf("Ratenzahlung: %.2f €%n", checkout.total(80));Überweisung: 78.40 €Ratenzahlung: 84.00 €Keine einzige Zeile des Checkouts hat sich geändert. Du kannst die Kartenzahlung nicht kaputt machen, während du Überweisungen hinzufügst, weil der Code der Karte nicht vor dir liegt.
Es gibt noch etwas. Wenn die neue Klasse die Methode vergisst oder ihren Namen falsch schreibt, lässt sich das Programm nicht kompilieren:
class Installments implements PaymentMethod { public double totl(double amount) { return amount * 1.05; }}error: Installments is not abstract and does not override abstract method total(double) in PaymentMethodBei der Kette von ifs zeigt sich ein falsch geschriebener Methodenname erst, wenn ein Kunde diese Art wählt.
Sieh es dir an
Welche Art der Kunde wählt
Der Kunde tippt die Art als Text. Die Kette von ifs, die die Regel gewählt hat, wird zu einer Tabelle: Name → Strategy.
Map<String, PaymentMethod> methods = Map.of( "Karte", new Card(), "Nachnahme", new CashOnDelivery(), "Gutschein", new Voucher(10));
Scanner scanner = new Scanner(System.in);String choice = scanner.nextLine();PaymentMethod method = methods.get(choice);if (method == null) { System.out.println("Keine solche Zahlungsart: " + choice);} else { Checkout checkout = new Checkout(method); System.out.printf("%s: %.2f €%n", choice, checkout.total(80));}NachnahmeNachnahme: 82.50 €Eine unbekannte Art erreicht den Checkout nie:
PayPalKeine solche Zahlungsart: PayPalDie Wahl steht weiterhin an einer Stelle, aber jetzt sind es Daten. Eine neue Art ist eine neue Zeile in der Tabelle und eine neue Klasse.
Genauer, für Neugierige
PaymentMethod hat eine Methode, ist also ein funktionales Interface, und eine Strategy kann auch ein Lambda sein:
Checkout checkout = new Checkout(amount -> amount + 2.50);System.out.printf("Nachnahme: %.2f €%n", checkout.total(80));Nachnahme: 82.50 €Ein Lambda genügt für eine Regel in einer Zeile. Eine Klasse lohnt sich, wenn die Strategy ein eigenes Feld (den Gutschein), einen Namen oder mehr als eine Methode hat.
Probier es aus
Sag die Ausgabe voraus
Die Bestellung kostet 8 €, und der Gutschein 10 €. Dann wechselt der Checkout zu Nachnahme. Was gibt das Programm aus?
Checkout checkout = new Checkout(new Voucher(10));System.out.printf("%.2f €%n", checkout.total(8));
checkout.setMethod(new CashOnDelivery());System.out.printf("%.2f €%n", checkout.total(8));Probier es aus
Finde den Fehler
Ein Gutschein soll einmal eingelöst werden. Jemand „verbraucht“ ihn mitten in der Strategy:
class Voucher implements PaymentMethod { private double value;
public Voucher(double value) { this.value = value; }
@Override public double total(double amount) { double result = Math.max(amount - value, 0); value = 0; return result; }}Checkout checkout = new Checkout(new Voucher(10));System.out.printf("Bestellvorschau: %.2f €%n", checkout.total(80));System.out.printf("Zahlung: %.2f €%n", checkout.total(80));Bestellvorschau: 70.00 €Zahlung: 80.00 €Die Vorschau zeigte 70 €, und die Zahlung verlangte 80 €. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Der Checkout ruft die Strategy jedes Mal auf, wenn er den Betrag braucht: für die Vorschau, für die Rechnung, für die Zahlung. Die Strategy soll nur rechnen. Sobald sie sich selbst ändert, bekommt die zweite Frage eine andere Antwort. Entferne die Zeile: Der Betrag ist beide Male 70 €. Ob der Gutschein eingelöst wurde, wird in der Bestellung festgehalten, nachdem die Zahlung durch ist.
Die Idee
Wann du es nicht brauchst
Strategy hat seinen Preis: ein Interface und eine Klasse für jede Variante statt eines if. Es lohnt sich nicht, wenn:
- es zwei Varianten gibt und sie sich nicht ändern. „Mit Mehrwertsteuer oder ohne“ ist ein
if; - die Regel an einer Stelle benutzt wird und niemand sie anfasst;
- es nur eine Strategy gibt, „auf Verdacht“. Ein Interface mit einer Implementierung ist Code, den du lesen musst, ohne etwas dafür zu bekommen.
Es lohnt sich, wenn die Varianten wachsen, sich ändern, während das Programm läuft (der Kunde wählt), oder wenn verschiedene Teile des Programms verschiedene Regeln brauchen.
Übung
Apple Pay, ohne den Checkout anzufassen
Löse die Aufgabe in deinem Editor.
Apple Pay. Der Shop bekommt Apple Pay mit 1 % Gebühr auf den Betrag. Füge eine Klasse ApplePay hinzu und bezahle damit eine Bestellung über 80 €. Der Checkout und die anderen Arten werden nicht angefasst.
Apple Pay: 80.80 €Weitere Aufgaben
- Nachnahme kostenlos über 100 €. Schreibe eine Nachnahme-Strategy, die bei Bestellungen über 100 € keine 2,50 € berechnet. Führe sie mit 80 € und mit 120 € aus.
- Die Art aus der Eingabe. Füge Überweisung und Ratenzahlung zur Tabelle von der Karte „Welche Art der Kunde wählt“ hinzu. Lies auch den Betrag aus der Eingabe.
- Eine Geschenkkarte mit Guthaben. Die Karte hat ein Guthaben von 50 €: Sie zahlt, was sie kann, und den Rest zahlt die Bankkarte. Überlege, wo das neue Guthaben festgehalten wird. Ändert es die Strategy oder die Bestellung nach der Zahlung?
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Werfen und eigene Exceptions · HashMap und HashSet · Lambdas · Entwurfsmuster
- Geht weiter in: Observer · Factory Method und Simple Factory · Singleton und Dependency Injection · Adapter und Facade
- Aufgaben zum Thema: Entwurfsmuster