Zum Inhalt
wannacode

Strategy

Kurz gesagt: Strategy legt jede Regel (etwa eine Zahlungsart) in eine eigene Funktion, 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:

function total(method, amount) {
if (method === "Karte") {
return amount;
} else if (method === "Nachnahme") {
return amount + 2.5;
} else if (method === "Gutschein") {
return Math.max(amount - 10, 0);
} else if (method === "Überweisung") {
return amount * 0.98;
} else if (method === "Ratenzahlung") {
return amount * 1.05;
}
throw new Error("Unbekannte Zahlungsart: " + method);
}
console.log(`Karte: ${total("Karte", 80).toFixed(2)} €`);
console.log(`Nachnahme: ${total("Nachnahme", 80).toFixed(2)} €`);
console.log(`Gutschein: ${total("Gutschein", 80).toFixed(2)} €`);
console.log(`Überweisung: ${total("Überweisung", 80).toFixed(2)} €`);
console.log(`Ratenzahlung: ${total("Ratenzahlung", 80).toFixed(2)} €`);
Ausgabe
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 Funktion. 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 Funktion 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 eine eigene Funktion. Alle Arten haben eine gemeinsame Form: Sie nehmen einen Betrag und geben zurück, wie viel zu zahlen ist. Der Checkout hält die Funktion in einem Feld und ruft sie auf, ohne zu wissen, welche Funktion 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: eine Funktion, die einen Betrag nimmt und einen Betrag zurückgibt;
  • die konkreten Strategies: eine Funktion 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

Drei Arten, jede in einer eigenen Funktion. Der Gutschein ist eine Funktion, die eine Funktion zurückgibt: voucher(10) erzeugt eine Strategy, die sich ihre 10 € merkt. So ist der Wert kein Parameter jeder Art:
function card(amount) {
return amount;
}
function cashOnDelivery(amount) {
return amount + 2.5;
}
function voucher(value) {
return (amount) => Math.max(amount - value, 0);
}
Der Checkout hält die Funktion im Feld method und ruft sie auf. Eine Zuweisung an checkout.method wechselt sie:
class Checkout {
constructor(method) {
this.method = method;
}
total(amount) {
return Math.round(this.method(amount) * 100) / 100;
}
}
const checkout = new Checkout(card);
console.log(`Karte: ${checkout.total(80).toFixed(2)} €`);
checkout.method = cashOnDelivery;
console.log(`Nachnahme: ${checkout.total(80).toFixed(2)} €`);
checkout.method = voucher(10);
console.log(`Gutschein: ${checkout.total(80).toFixed(2)} €`);
Ausgabe
Karte: 80.00 €
Nachnahme: 82.50 €
Gutschein: 70.00 €

const checkout = new Checkout(card);
console.log(`Karte: ${checkout.total(80).toFixed(2)} €`);
checkout.method = cashOnDelivery;
console.log(`Nachnahme: ${checkout.total(80).toFixed(2)} €`);
checkout.method = voucher(10);
console.log(`Gutschein: ${checkout.total(80).toFixed(2)} €`);
Schritt für Schritt, in Worten
  1. Der Checkout bekommt eine Karte: sein Feld method zeigt auf die Funktion card.
  2. total(80): Der Checkout kennt die Zahlungsart nicht. Er fragt die Strategy nur nach dem Betrag und bekommt 80.00 zurück.
  3. Eine neue Art. checkout.method = … stellt das Feld auf die Funktion cashOnDelivery um. In Checkout ändert sich keine einzige Zeile.
  4. Dasselbe total(80), eine andere Antwort: 82.50.
  5. voucher(10) gibt eine Funktion zurück, die sich value = 10 merkt. Auf die alten Funktionen zeigt nichts mehr.
  6. 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 Funktionen:

function bankTransfer(amount) {
return amount * 0.98;
}
function installments(amount) {
return amount * 1.05;
}
const checkout = new Checkout(bankTransfer);
console.log(`Überweisung: ${checkout.total(80).toFixed(2)} €`);
checkout.method = installments;
console.log(`Ratenzahlung: ${checkout.total(80).toFixed(2)} €`);
Ausgabe
Ü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.

Hier gibt es keinen Compiler, der die Form prüft. Wenn die neue Funktion zwei Werte statt einem nimmt, zeigt sich der Fehler erst, wenn der Checkout sie aufruft. Darum läuft jede neue Strategy mindestens einmal, bevor sie einen Kunden erreicht.

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.

const methods = new Map([
["Karte", card],
["Nachnahme", cashOnDelivery],
["Gutschein", voucher(10)],
]);
const choice = await ask("");
rl.close();
const method = methods.get(choice);
if (method === undefined) {
console.log("Keine solche Zahlungsart: " + choice);
} else {
const checkout = new Checkout(method);
console.log(`${choice}: ${checkout.total(80).toFixed(2)} €`);
}
Eingabe
Nachnahme
Ausgabe
Nachnahme: 82.50 €

Eine unbekannte Art erreicht den Checkout nie:

Eingabe
PayPal
Ausgabe
Keine solche Zahlungsart: PayPal

Die Wahl steht weiterhin an einer Stelle, aber jetzt sind es Daten. Eine neue Art ist eine neue Zeile in der Tabelle und eine neue Funktion.

Genauer, für Neugierige

Warum eine Funktion und keine Klasse? In JavaScript ist eine Funktion ein Wert: Sie wird wie jedes andere Objekt in einem Feld und in einer Tabelle gehalten. Eine Strategy mit einer Methode ist genau eine Funktion. Eine Klasse (oder ein Objekt mit Methoden) lohnt sich, wenn die Strategy mehr als eins tut: zum Beispiel einen Betrag und eine Zeile für die Rechnung. In Java und C# schreibt man dasselbe mit einem Interface und einer Klasse pro Art.

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?

const checkout = new Checkout(voucher(10));
console.log(`${checkout.total(8).toFixed(2)} €`);
checkout.method = cashOnDelivery;
console.log(`${checkout.total(8).toFixed(2)} €`);
Antwort

Probier es aus

Finde den Fehler

Ein Gutschein soll einmal eingelöst werden. Jemand „verbraucht“ ihn mitten in der Strategy:

function voucher(value) {
return (amount) => {
const result = Math.max(amount - value, 0);
value = 0;
return result;
};
}
const checkout = new Checkout(voucher(10));
console.log(`Bestellvorschau: ${checkout.total(80).toFixed(2)} €`);
console.log(`Zahlung: ${checkout.total(80).toFixed(2)} €`);
Ausgabe
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.

Die Idee

Wann du es nicht brauchst

Strategy hat seinen Preis: eine eigene Funktion für jede Variante und eine Tabelle 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 Funktion gibt und die Tabelle „auf Verdacht“ da ist.

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 Funktion applePay hinzu und bezahle damit eine Bestellung über 80 €. Der Checkout und die anderen Arten werden nicht angefasst.

Ausgabe
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

1. Der Shop bekommt Apple Pay. Was schreibst du mit Strategy?
2. Was hält der Checkout bei Strategy?
3. Die Gutschein-Funktion setzt ihren Wert bei jedem Aufruf auf null. Der Checkout fragt einmal für die Vorschau und einmal für die Zahlung. Was passiert?
4. Wann brauchst du Strategy nicht?

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