Zum Inhalt
wannacode

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:
def __init__(self):
self.orders = []
def place_order(self, id, customer, amount, method):
if method == "Karte":
total = amount
elif method == "Nachnahme":
total = amount + 2.50
else:
raise ValueError("Unbekannte Zahlungsart: " + method)
self.orders.append(f"{id};{customer};{total}")
print(f"E-Mail an {customer}: Bestellung {id}, {total:.2f} €")
print("Lager: bereite vor " + id)
def count(self):
return len(self.orders)
shop = Shop()
shop.place_order("A-1", "ana@example.com", 80, "Karte")
shop.place_order("A-2", "ivan@example.com", 45, "Nachnahme")
print("Bestellungen: " + str(shop.count()))
Ausgabe
E-Mail an ana@example.com: Bestellung A-1, 80.00 €
Lager: bereite vor A-1
E-Mail an ivan@example.com: Bestellung A-2, 47.50 €
Lager: bereite vor A-2
Bestellungen: 2

Es 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.

1. Eine Kette von ifs auf den Text der Zahlungsart, die mit jeder neuen Art wächst. Welches Muster ordnet sie?
2. Nach einer Bestellung schickt die Methode selbst eine E-Mail und schreibt ins Lager. Morgen kommt noch SMS dazu. Welches Muster?
3. Die Bestellungen liegen in einer Liste im Shop. Morgen brauchst du eine Datenbank, und für den Test willst du etwas im Speicher. Welches Muster?
4. Die Bestellung hat drei Felder. Braucht sie einen Builder?

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: Funktionen in einem dict. Die Bestellung ist eine unveränderliche dataclass mit drei Feldern. Die Speicherung ist eine Klasse mit zwei Methoden, save und count:
from dataclasses import dataclass
@dataclass(frozen=True)
class Order:
id: str
customer: str
total: float
class InMemoryOrders:
def __init__(self):
self._orders = []
def save(self, order):
self._orders.append(order)
def count(self):
return len(self._orders)
Der Shop bekommt die Speicherung und die Tabelle der Arten von außen. Er führt eine Liste von Abonnenten und sagt jedem Bescheid, wenn eine Bestellung aufgegeben wird:
class Shop:
def __init__(self, orders, methods):
self.orders = orders
self.methods = methods
self.listeners = []
def subscribe(self, listener):
self.listeners.append(listener)
def place_order(self, id, customer, amount, method):
payment = self.methods.get(method)
if payment is None:
raise ValueError("Unbekannte Zahlungsart: " + method)
order = Order(id, customer, payment(amount))
self.orders.save(order)
for listener in self.listeners:
listener(order)
def count(self):
return self.orders.count()

Welche Speicherung, welche Arten und welche Abonnenten, wird an einer Stelle entschieden, dort, wo der Shop zusammengesetzt wird:

methods = {
"Karte": lambda amount: amount,
"Nachnahme": lambda amount: amount + 2.50,
}
shop = Shop(InMemoryOrders(), methods)
shop.subscribe(lambda o: print(f"E-Mail an {o.customer}: Bestellung {o.id}, {o.total:.2f} €"))
shop.subscribe(lambda o: print("Lager: bereite vor " + o.id))
shop.place_order("A-1", "ana@example.com", 80, "Karte")
shop.place_order("A-2", "ivan@example.com", 45, "Nachnahme")
print("Bestellungen: " + str(shop.count()))
Ausgabe
E-Mail an ana@example.com: Bestellung A-1, 80.00 €
Lager: bereite vor A-1
E-Mail an ivan@example.com: Bestellung A-2, 47.50 €
Lager: bereite vor A-2
Bestellungen: 2

Die 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 der Speicherung (Methoden save und count). Keine einzige Zeile der Shop-Klasse ändert sich:

methods = {
"Karte": lambda amount: amount,
"Nachnahme": lambda amount: amount + 2.50,
"Überweisung": lambda amount: amount * 0.98,
}
shop = Shop(InMemoryOrders(), methods)
shop.subscribe(lambda o: print("SMS an " + o.customer + ": " + o.id))
shop.place_order("A-3", "ana@example.com", 100, "Überweisung")
Ausgabe
SMS an ana@example.com: A-3

Jetzt 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

1. Du ordnest den Code mit Mustern. Woher weißt du, dass du nichts kaputt gemacht hast?
2. Welcher Grund ist KEIN guter Grund, ein Muster einzubauen?
3. Nach dem Ordnen fügst du Überweisung und SMS hinzu. Was fasst du in der Shop-Klasse an?

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